Fiber的优先级调治机制与事件系统,专项论题复习

2020-02-25 作者:网站首页   |   浏览(147)

时间: 2019-12-30阅读: 55标签: 机制

放自己用DOM写的作品链接
写于2017.08.07
一、关于DOM

经典的事件系统分成两大块,绑定事件与分派事件,在浏览器中,分派事件很少人会直接dispatchEvent。因为创建一个DOM 事件是非常复杂的事情,不同的事件对象对应不同的事件构造器,传参也五花八门。因为分派事件基本上用户行为触发,比如我们点击了某个元素,恰逢在这上方绑定了点击事件,于是触发了。

  • 什么是DOM
    DOM是JavaScript操作网页的接口,全称为“文档对象模型”(Document Object Model)。它的作用是将网页转为一个JavaScript对象,从而可以用脚本进行各种操作(比如增删内容)。
    浏览器会根据DOM模型,将结构化文档(比如HTML和XML)解析成一系列的节点,再由这些节点组成一个树状结构(DOM Tree)。所有的节点和最终的树状结构,都有规范的对外接口。所以,DOM可以理解成网页的编程接口。
    严格地说,DOM不属于JavaScript,但是操作DOM是JavaScript最常见的任务,而JavaScript也是最常用于DOM操作的语言
  • DOM的节点
    DOM的最小组成单位叫做节点(node)。文档的树形结构(DOM树),就是由各种不同类型的节点组成。每个节点可以看作是文档树的一片叶子。
    节点的类型有七种。
    *Document:整个文档树的顶层节点
    *DocumentType:doctype标签(比如<!DOCTYPE html>)
    *Element:网页的各种HTML标签(比如<body>、<a>等)
    *Attribute:网页元素的属性(比如class="right")
    *Text:标签之间或标签包含的文本
    *Comment:注释
    *DocumentFragment:文档的片段
    DOM树如下图:

React的绑定事件是在JSX 中进行,换言之, render时,props的onXXX事件就被收集起来,进行绑定。在jQuery时,人们发明了事件委托,将可以冒泡的事件挂在document或window对象上,进行统一的监听,实现高性能的事件系统。现在我们可以无视IE8 ,因此对不可冒泡的事件可以使用事件捕获,对能冒泡的事件使用事件冒泡,也是放在顶层对象 上进行监听。

图片 1

function trapBubbledEvent(topLevelType, element) { //by 司徒正美 trapEventForPluginEventSystem(element, topLevelType, false);}function trapCapturedEvent(topLevelType, element) { trapEventForPluginEventSystem(element, topLevelType, true);}

image.png

大家可能觉得很奇怪,浏览器的dom.addEventListener(type, fn, capture)怎么也要三个参数, 原因是fn是一个统一调度的方法,因此可省掉一个。而冒泡与捕获则通过不同的方法做区分,实际干活的是trapEventForPluginEventSystem。

具体DOM学习建议参考链接:http://javascript.ruanyifeng.com/#dom
二、事件模型

trapEventForPluginEventSystem根据事件名进行分大三类,DiscreteEvent,UserBlockingEvent,ContinuousEvent,选出不同的事件派发器。

  • 什么是事件
    事件是一种异步编程的实现方式,本质上是程序各个组成部分之间的通信。一般来讲,有一下特点:
    1、某某订阅了/关注/监听了×××
    2、×××发生变化
    3、某某得到通知
    体现在DOM机制里就是:用户的操作发生变化,代码得到通知

  • 事件EventTarget接口
    DOM的事件操作(监听和触发),都定义在EventTarget接口。Element节点、document节点和window对象,都部署了这个接口。此外,XMLHttpRequest、AudioNode、AudioContext等浏览器内置对象,也部署了这个接口。
    该接口就是3个方法:
    1、addEventListener:绑定事件的监听函数
    例:
    target.addEventListener(type, listener[, useCapture]);
    type:事件名称,大小写敏感。
    listener:监听函数。事件发生时,会调用该监听函数。
    useCapture:布尔值,默认为false(监听函数只在冒泡阶段被触发)。true是在捕获阶段触发
    addEventListener方法可以为当前对象的同一个事件,添加多个监听函数。这些函数按照添加顺序触发,即先添加先触发。如果为同一个事件多次添加同一个监听函数,该函数只会执行一次,多余的添加将自动被去除(不必使用removeEventListener方法手动去除)。
    2、removeEventListener:移除事件的监听函数
    例:
    div.addEventListener('click', listener, false);
    removeEventListener方法移除的监听函数,必须与对应的addEventListener方法的参数完全一致,而且必须在同一个元素节点,否则无效。
    3、dispatchEvent:触发事件
    dispatchEvent方法在当前节点上触发指定事件,从而触发监听函数的执行。该方法返回一个布尔值,只要有一个监听函数调用了Event.preventDefault(),则返回值为false,否则为true
    例:
    para.addEventListener('click', hello, false);
    var event = new Event('click');
    para.dispatchEvent(event);
    上面代码在当前节点触发了click事件

  • 3种绑定监听函数的方法
    1、HTML标签的on-属性
    例:元素标签的属性中,直接定义某些事件的监听代码
    <body onload="doSomething()">
    <div onclick="console.log('触发事件')">
    上面代码为body节点的load事件、div节点的click事件,指定了监听函数。
    使用这个方法指定的监听函数,只会在冒泡阶段触发

    另外,Element元素节点的setAttribute方法,其实设置的也是这种效果。
    el.setAttribute('onclick', 'doSomething()');
    2、Element节点的事件属性
    Element节点对象有事件属性,同样可以指定监听函数。
    例:
    window.onload = doSomething;
    div.onclick = function(event){
    console.log('触发事件');
    };
    使用这个方法指定的监听函数,只会在冒泡阶段触发。
    3、addEventListener方法
    window.addEventListener('load', doSomething, false);(详解如上)
    总结:第一种“HTML标签的on-属性”,违反了HTML与JavaScript代码相分离的原则;第二种“Element节点的事件属性”的缺点是,同一个事件只能定义一个监听函数,也就是说,如果定义两次onclick属性,后一次定义会覆盖前一次。

  • 事件的捕获与冒泡阶段
    1、操作系统最先知道用户点击了鼠标,浏览器次之
    2、child被点击了,意味着parent也被点击了
    3、那么如果同时监听了child和parent,谁先通知我
    例子:如果对parent和child同时设置了监听,那么当我点击child,谁先通知我
    捕获阶段:parent先通知,child后通知(一般不用)
    冒泡阶段:child先通知,parent后通知

  • e.target与e.currentTarget
    e.target代表你点击的那个元素,而e.currentTarget代表你监听的那个元素,如果你处于捕获或者冒泡阶段,二者是不一样的

  • 阻止默认事件
    document.querySelector("a").addEventListener('click',function(e){
    e.preventDefault()
    })
    这样的话a就不会跳转了。如果在父元素阻止默认事件,那a也不跳转了,所以一般不建议这样做

  • 停止冒泡
    e.stopPropagation( )(在子元素加入,则不通知爸爸了,不会再向上传播)

  • 事件委托
    由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件委托
    例:
    HTML代码:
    <ul>
    <li>选项1</li>
    <li>选项2</li>
    <li>选项3</li>
    <li>选项4</li>
    </ul>
    JS代码:
    var ul =document.querySelector('ul')
    function fn(e){
    if(e.target.tagName ==="LI"){
    console.log("ok")(如果li里面裹一层span,点击span就不能执行监听函数)
    }
    }
    ul.addEventListener('click',fn)
    优化执行函数:
    function fn(e){
    var el = e.target
    while(el.tagName !=="LI"){
    el =el.parentNode(向上遍历父元素)
    }
    if(el){
    console.log('yes')
    }
    }
    但是实际上我只需要遍历到我监听的元素就可以了,如果直到遍历到监听的元素还没有的话,就认为没有了
    例:
    HTML代码:
    <div>
    <p>我是<span>p</span></p>
    <hi>我是<span>h1</span></hi>
    </div>
    JS代码:
    var div =document.querySelector('div')
    function fn(e){
    var el = e.target
    while(el.tagName !=="H1"){
    el =el.parentNode(向上遍历父元素)
    if(el===div){
    el=null
    break;
    }
    }
    if(el){
    console.log('yes')
    }
    }
    div.addEventListener('click',fn(e))

function trapEventForPluginEventSystem(element, topLevelType, capture) { //by 司徒正美 var listener; switch (getEventPriority(topLevelType)) { case DiscreteEvent: listener = dispatchDiscreteEvent.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM); break; case UserBlockingEvent: listener = dispatchUserBlockingUpdate.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM); break; case ContinuousEvent: //by 司徒正美 default: listener = dispatchEvent.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM); break; } var rawEventName = getRawEventName(topLevelType); if (capture) { addEventCaptureListener(element, rawEventName, listener); } else { addEventBubbleListener(element, rawEventName, listener); }}

DiscreteEvent 离散事件. 例如blur、focus、 click、 submit、 touchStart. 这些事件都是离散触发的。

UserBlockingEvent 用户阻塞事件. 例如touchMove、mouseMove、scroll、drag、dragOver等等。这些事件会'阻塞'用户的交互。

ContinuousEvent 连续事件。例如load、error、loadStart、abort、animationEnd. 这个优先级最高,也就是说它们应该是立即同步执行的,这就是Continuous的意义,是持续地执行,不能被打断。

源码里有一个怒长的表,对所有常用事件进行归类

var eventTuples = [// Discrete events[TOP_BLUR, 'blur', DiscreteEvent], [TOP_CANCEL, 'cancel', DiscreteEvent], [TOP_CLICK, 'click', DiscreteEvent], [TOP_CLOSE, 'close', DiscreteEvent], [TOP_CONTEXT_MENU, 'contextMenu', DiscreteEvent], [TOP_COPY, 'copy', DiscreteEvent], [TOP_CUT, 'cut', DiscreteEvent], [TOP_AUX_CLICK, 'auxClick', DiscreteEvent], [TOP_DOUBLE_CLICK, 'doubleClick', DiscreteEvent], [TOP_DRAG_END, 'dragEnd', DiscreteEvent], [TOP_DRAG_START, 'dragStart', DiscreteEvent], [TOP_DROP, 'drop', DiscreteEvent], [TOP_FOCUS, 'focus', DiscreteEvent], [TOP_INPUT, 'input', DiscreteEvent], [TOP_INVALID, 'invalid', DiscreteEvent], [TOP_KEY_DOWN, 'keyDown', DiscreteEvent], [TOP_KEY_PRESS, 'keyPress', DiscreteEvent], [TOP_KEY_UP, 'keyUp', DiscreteEvent], [TOP_MOUSE_DOWN, 'mouseDown', DiscreteEvent], [TOP_MOUSE_UP, 'mouseUp', DiscreteEvent], [TOP_PASTE, 'paste', DiscreteEvent], [TOP_PAUSE, 'pause', DiscreteEvent], [TOP_PLAY, 'play', DiscreteEvent], [TOP_POINTER_CANCEL, 'pointerCancel', DiscreteEvent], [TOP_POINTER_DOWN, 'pointerDown', DiscreteEvent], [TOP_POINTER_UP, 'pointerUp', DiscreteEvent], [TOP_RATE_CHANGE, 'rateChange', DiscreteEvent], [TOP_RESET, 'reset', DiscreteEvent], [TOP_SEEKED, 'seeked', DiscreteEvent], [TOP_SUBMIT, 'submit', DiscreteEvent], [TOP_TOUCH_CANCEL, 'touchCancel', DiscreteEvent], [TOP_TOUCH_END, 'touchEnd', DiscreteEvent], [TOP_TOUCH_START, 'touchStart', DiscreteEvent], [TOP_VOLUME_CHANGE, 'volumeChange', DiscreteEvent], // User-blocking events[TOP_DRAG, 'drag', UserBlockingEvent], [TOP_DRAG_ENTER, 'dragEnter', UserBlockingEvent], [TOP_DRAG_EXIT, 'dragExit', UserBlockingEvent], [TOP_DRAG_LEAVE, 'dragLeave', UserBlockingEvent], [TOP_DRAG_OVER, 'dragOver', UserBlockingEvent], [TOP_MOUSE_MOVE, 'mouseMove', UserBlockingEvent], [TOP_MOUSE_OUT, 'mouseOut', UserBlockingEvent], [TOP_MOUSE_OVER, 'mouseOver', UserBlockingEvent], [TOP_POINTER_MOVE, 'pointerMove', UserBlockingEvent], [TOP_POINTER_OUT, 'pointerOut', UserBlockingEvent], [TOP_POINTER_OVER, 'pointerOver', UserBlockingEvent], [TOP_SCROLL, 'scroll', UserBlockingEvent], [TOP_TOGGLE, 'toggle', UserBlockingEvent], [TOP_TOUCH_MOVE, 'touchMove', UserBlockingEvent], [TOP_WHEEL, 'wheel', UserBlockingEvent], // Continuous events[TOP_ABORT, 'abort', ContinuousEvent], [TOP_ANIMATION_END, 'animationEnd', ContinuousEvent], [TOP_ANIMATION_ITERATION, 'animationIteration', ContinuousEvent], [TOP_ANIMATION_START, 'animationStart', ContinuousEvent], [TOP_CAN_PLAY, 'canPlay', ContinuousEvent], [TOP_CAN_PLAY_THROUGH, 'canPlayThrough', ContinuousEvent], [TOP_DURATION_CHANGE, 'durationChange', ContinuousEvent], [TOP_EMPTIED, 'emptied', ContinuousEvent], [TOP_ENCRYPTED, 'encrypted', ContinuousEvent], [TOP_ENDED, 'ended', ContinuousEvent], [TOP_ERROR, 'error', ContinuousEvent], [TOP_GOT_POINTER_CAPTURE, 'gotPointerCapture', ContinuousEvent], [TOP_LOAD, 'load', ContinuousEvent], [TOP_LOADED_DATA, 'loadedData', ContinuousEvent], [TOP_LOADED_METADATA, 'loadedMetadata', ContinuousEvent], [TOP_LOAD_START, 'loadStart', ContinuousEvent], [TOP_LOST_POINTER_CAPTURE, 'lostPointerCapture', ContinuousEvent], [TOP_PLAYING, 'playing', ContinuousEvent], [TOP_PROGRESS, 'progress', ContinuousEvent], [TOP_SEEKING, 'seeking', ContinuousEvent], [TOP_STALLED, 'stalled', ContinuousEvent], [TOP_SUSPEND, 'suspend', ContinuousEvent], [TOP_TIME_UPDATE, 'timeUpdate', ContinuousEvent], [TOP_TRANSITION_END, 'transitionEnd', ContinuousEvent], [TOP_WAITING, 'waiting', ContinuousEvent]];

addEventBubbleListener与addEventCaptureListener就是对DOM 的addEvenListener的简单封装。但要注意的是,这两个方法都是使用注入方式实现的。换言之,react-native中,由于打包的文件不一样,它对应的实现也不一样

function addEventBubbleListener(element, eventType, listener) { element.addEventListener(eventType, listener, false); //by 司徒正美}function addEventCaptureListener(element, eventType, listener) { element.addEventListener(eventType, listener, true); //by 司徒正美}

然后我们终于有机会看dispatchDiscreteEvent,dispatchUserBlockingUpdate与dispatchEvent。

function dispatchDiscreteEvent(topLevelType, eventSystemFlags, nativeEvent) { flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp); discreteUpdates(dispatchEvent, topLevelType, eventSystemFlags, nativeEvent);} //by 司徒正美function dispatchUserBlockingUpdate(topLevelType, eventSystemFlags, nativeEvent) { runWithPriority(UserBlockingPriority, dispatchEvent.bind(null, topLevelType, eventSystemFlags, nativeEvent));}

派发离散事件分两部分,第一个是flushDiscreteUpdatesIfNeeded,你跟踪进去是flushDiscreteUpdates,它会搞定之前积攒的DiscreteEvent与useEffect回调。第二个是discreteUpdates,它会为React 的调度叠加一个DiscreteEventContext 上下文,并执行runWithPriority,这时看来它与dispatchUserBlockingUpdate无异,只是做了一个前置处理。

function discreteUpdates(fn, a, b, c) { var prevExecutionContext = executionContext; executionContext |= DiscreteEventContext; try { // Should this return runWithPriority(UserBlockingPriority, fn.bind(null, a, b, c)); } finally { executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch flushSyncCallbackQueue(); } }}

runWithPriority对调度器的影响主要有两个,一个是fiber节点的expirationTime属性,涉及到fiber对DOM的刷新,另一个是fiber节点上的回调的执行(setState, forceUpdate, hooks),它们会转交scheduleCallback方法处理。scheduleCallback相当于一个setTimeout,根据priorityLevel,延迟执行事件。scheduleCallback详看这里:​

priorityLevel能立即转换成callbackPriority,默认为NoPriority 90。然后运行过程中变成以下5种。

ImmediatePriority 99IMMEDIATE_PRIORITY_TIMEOUT=-1;UserBlockingPriority 98USER_BLOCKING_PRIORITY=250NormalPriority 97NORMAL_PRIORITY_TIMEOUT=5000LowPriority 96LOW_PRIORITY_TIMEOUT=10000IdlePriority 85LOW_PRIORITY_TIMEOUT=10000

每一个fiber都分配一个expirationTime属性(其实有多种expirationTime属性),它是大于当时的毫秒数。但调度器执行时,就计算出当前的毫秒数now, 然后now - fiber.expirationTime = 0,那么这fiber就可以更新了,其priorityLevel会改成ImmediatePriority。否则它只会创建一个effect记录用户的操作(如更新了某个属性啦,对它做删除啦)。

好了,我们知道runWithPriority重要性了,那么我们需要获取其他priorityLevel值。

我们会发现flushControlled,deferredUpdates,flushSync会包含runWithPriority(NormalPriority, fn)逻辑。syncUpdates, flushSyncCallback则包含runWithPriority(ImmediatePriority, fn)的逻辑。**

此外,SuspenseComponent也会影响到expirationTime的计算(默认分配为LOW_PRIORITY_EXPIRATION),最后通过 内部 inferPriorityFromExpirationTime的方法计算priorityLevel值。

本文React版本为 16.10.2总结:

多年之前,人们说到fiber,只是模糊地联想到Time slicing与Suspense这两个单词。而经过多次迭代,我们终于能揭开fiber的真面目。 React的时间切片,只是它更新的一种表现,实质上是由每个fiber的expirationTime所决定,而fiber的expirationTime又来自priorityLevel,priorityLevel则来自用户的UI操作,不同的事件,带来三种不同的priorityLevel。而悬停,则只为某个fiber带来第四种priorityLevel——LowPriority。用户代码出现问题,被catch住时,出现第五种priorityLevel——IdlePriority。

作者:司徒正美原文:

本文由yzc216亚洲城发布于网站首页,转载请注明出处:Fiber的优先级调治机制与事件系统,专项论题复习

关键词: yzc216亚洲城