写意风流

写意风流 查看完整档案

南京编辑南京工业大学  |  应用化学 编辑南京帆软  |  前端开发 编辑填写个人主网站
编辑

诸事烦于心今日岂能浪迹天涯,
事了拂衣去明朝我必四海为家。

个人动态

写意风流 关注了专栏 · 2018-11-01

全栈修仙之路

聚焦全栈,专注分享 Angular、TypeScript、Node.js/Java 、Spring 技术栈等全栈干货。 欢迎小伙伴们关注公众号全栈修仙之路,一起升级打怪。

关注 6858

写意风流 关注了专栏 · 2018-10-16

前端精读专栏

精读前端业界好文,每周更新

关注 6501

写意风流 发布了文章 · 2018-10-12

event loop 与 vue

结论

对于event loop 可以抽象成一段简单的代码表示

for (macroTask of macroTaskQueue) {
    // 1. Handle current MACRO-TASK
    handleMacroTask();
      
    // 2. Handle all MICRO-TASK
    for (microTask of microTaskQueue) {
        handleMicroTask(microTask);
    }
}

js事件机制

javascript是一个单线程语言,同一时间只能执行一个任务。
对于javascript的事件处理机制,我们可以简单理解成“主线程+任务队列”模式。主要步骤如下

(1)所有同步任务都在主线程上执行,形成一个执行栈。

(2)主线程之外,还存在一个 "任务队列"(task queue)。只要异步任务有了运行结果,就在 "任务队列" 之中放置一个事件。
(3)一旦 "执行栈" 中的所有同步任务执行完毕,系统就会读取 "任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

任务队列

任务队列分为task queue和microtask queue。执行栈任务清空后会先从Microtasks中取任务,Microtasks中执行完之后才会执行task中的任务。

因此一个event loop主要流程如下:

  1. 开始一个Event loop
  2. 执行栈从tasks queue中取任务,并执行。
  3. 执行完后,执行栈清空
  4. 执行栈从microtasks queue中取任务执行
  5. 执行完成,执行栈清空
  6. 判断microtasks queue是否还有任务,有则重复步骤3。
  7. 进入 Update the rendering(更新渲染)阶段
  8. Event loop结束

流程图如下:

clipboard.png

再仔细理解一下开头列出的代码:

for (macroTask of macroTaskQueue) {
    // 1. Handle current MACRO-TASK
    handleMacroTask();
      
    // 2. Handle all MICRO-TASK
    for (microTask of microTaskQueue) {
        handleMicroTask(microTask);
    }
}

VUE与EVENT_LOOP

为什么使用Microtask queue

对于VUE这类web2.0框架而言,最主要做的应该还是把对data的修改映射到DOM上。如果只修改一个数据,就刷新一次DOM,那么在一个同步过程中,同时修改好几个数据,必然会导致多次渲染。这肯定是不可取的。
从上面的event loop我们了解到,一个event loop对应一次render。理想状况当然就是,一次event loop产生的所有改动最好再render之前将DOM都先更新好。这样在一个周期中就可以只render一次。

从这点需求出发很容易发现,microtask queue很符合要求。

  1. microtask queue肯定在render ui之前执行完
  2. 不像task queue存在多个,microtask只存在一个queue。
  3. 最关键的是: microtask queue只有都清空了才能进入下一步,无论queue里是什么时候塞进来的。

实际代码中,不管是鼠标点击还是键盘输入或是网络时间,触发了哪些方法,这些触发都可以看成开启一个event loop。这些触发造成的任何修改都放到microtask queue中,就可以保证在这一轮的evnet loop走到render ui时可以拿到最新的DOM。

要说明的是,这里的render并不是维护虚拟DOM,也不是把虚拟DOM的变化投射到真实DOM上。而是将真实DOM更新到UI的过程。

这么说是因为:Event Loop 并不是在 ECMAScript 标准中定义的,而是在 HTML 标准中定义的:

To coordinate events, user interaction, scripts, rendering, networking, and so forth...

在 JavaScript Engine 中(以 V8 为例),只是实现了 ECMAScript 标准,而并不关心什么 Event Loop。也就是说 Event Loop 是属于 JavaScript Runtime 的,是由宿主环境提供的(比如浏览器)。
浏览器可不会关心什么虚拟DOM。只负责DOM改变后渲染UI。

为什么不用task queue

同上: 在开启一个event loop后,如果将任务放到task queue中,那么这个task任务只会在本轮Event loop结束后才会执行,并开启新一轮event loop。这无疑会导致两次render UI。
实际上,尤大为了修复一些bug,曾经将VUE.nexttick用task queue实现。但是导致了很明显的性能问题。
可以看看两个列子: 例一 , 例二
两个fiddle的实现一模一样,就是让那个绝对定位的黄色元素起到一个fixed定位的效果:绑定scroll事件,每次滚动的时候,计算当前滚动的位置并更改到那个绝对定位元素的top属性上去。大家自己试试滚动几下,对比下效果,你就会发现第一个fiddle中的黄元素是稳定不动的,fixed很好。而后一个fiddle中就有问题了,黄色元素上下晃动,似乎跟不上我们scroll的节奏,总要慢一点,虽然最后停下滚动时位置是对的。

上述两个例子其实是在这个issue中找到的,第一个jsfiddle使用的版本是Vue 2.0.0-rc.6,这个版本的nextTick实现是采用了MO,而后因为IOS9.3的WebView里的MO有bug,于是尤雨溪更改了实现,换成了window.postMessage,也就是后一个fiddle所使用的Vue 2.0.0-rc.7。后来尤雨溪了解到window.postMessage是将回调放入的macrotask 队列。这就是问题的根源了。

参考:

  1. Tasks, microtasks, queues and schedules
  2. 深入理解 JavaScript Event Loop
  3. Vue源码详解之nextTick:MutationObserver只是浮云,microtask才是核心!
查看原文

赞 2 收藏 1 评论 0

写意风流 关注了专栏 · 2018-10-05

腾讯云技术社区

最专业的云解读社区

关注 11483

写意风流 发布了文章 · 2018-09-25

源码学习VUE之Watcher

我们在前面推导过程中实现了一个简单版的watcher。这里面还有一些问题

class Watcher {
    constructors(component, getter, cb){
        this.cb = cb // 对应的回调函数,callback
        this.getter = getter;
        this.component = component; //这就是执行上下文
    }
    
    //收集依赖
    get(){
        Dep.target = this;        
        this.getter.call(this.component)   
        if (this.deep) {
            traverse(value)
        }
        Dep.target = null;
    }
    
    update(){
        this.cb()
    }
}

同步异步更新

所谓的同步更新是指当观察的主体改变时立刻触发更新。而实际开发中这种需求并不多,同一事件循环中可能需要改变好几次state状态,但视图view只需要根据最后一次计算结果同步渲染就行(react中的setState就是典型)。如果一直做同步更新无疑是个很大的性能损耗。
这就要求watcher在接收到更新通知时不能全都立刻执行callback。我们对代码做出相应调整

constructors(component, getter, cb, options){
        this.cb = cb // 对应的回调函数,callback
        this.getter = getter;
        this.id = UUID() // 生成一个唯一id
        this.sync = options.sync; //默认一般为false
        this.vm = component; //这就是执行上下文
        this.value = this.getter() // 这边既收集了依赖,又保存了旧的值
    }
        
    update(){
        if(this.sync){ //如果是同步那就立刻执行回调
            this.run();
        }else{
            // 否则把这次更新缓存起来
            //但是就像上面说的,异步更新往往是同一事件循环中多次修改同一个值,
            // 那么一个wather就会被缓存多次。因为需要一个id来判断一下,
            queueWatcher(this)
        }
    }
    
    run: function(){
        //获取新的值
        var newValue = this.getter();
        this.cb.call(this.vm, newValue, this.value)
    }

这里的一个要注意的地方是,考虑到极限情况,如果正在更新队列中wather时,又塞入进来该怎么处理。因此,加入一个flushing来表示队列的更新状态。
如果加入的时候队列正在更新状态,这时候分两种情况:

  1. 这个watcher已经更新过, 就把这个watcher再放到当前执行的下一位,当前watcher处理完,立即处理这个最新的。
  2. 这个watcher还没有处理,就找到这个wather在队列中现有的位置,并再把新的放在后面。
let flushing = false;
let has = {}; // 简单用个对象保存一下wather是否已存在
function queueWatcher (watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true // 如果之前没有,那么就塞进去吧,如果有了就不用管了
    if (!flushing) {
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
   // ... 等同一事件循环结束后再依次处理队列里的watcher。具体代码放到后面nexttick部分再说
    }
  }
}

这么设计不无道理。我们之所以为了将wather放入队列中,就是为了较少不必要的操作。考虑如下代码

data: {
    a: 1
},
computed: {
    b: function(){
        this.a + 1
    }
}

methods: {
    act: function(){
        this.a = 2;
        // do someting
        this.a = 1
    }
}

在act操作中,我们先改变a,再把它变回来。我们理想状况下是a没变,b也不重新计算。这就要求,b的wather执行update的时候要拿到a最新的值来计算。这里就是1。如果队列中a的watehr已经更新过,那么就应该把后面的a的wather放到当前更新的wather后面,立即更新。这样可以保证后面的wather用到a是可以拿到最新的值。
同理,如果a的wather还没有更新,那么把新的a的wather放的之前的a的wather的下一位,也是为了保证后面的wather用到a是可以拿到最新的值。

computed

之所以把计算属性拿出爱单独讲,是因为

  1. 计算属性存在按需加载的情况
  2. 与render和$watcher相比,计算属性a可能依赖另一个计算属性b。

按需加载

所谓的按需计算顾名思义就是用到了才会计算,即调用了某个计算属性的get方法。在前面的方法中,我们在class Watcher的constructor中直接调用了getter方法收集依赖,这显然是不符合按需加载的原则的。

依赖收集

实际开发中,我们发现一个计算属性往往由另一个计算属性得来。如,

computed: {
    a: function(){
        return this.name;
    },
    b: function(){
        return this.a + "123"; 
    }
}

对于a而言,它是b的依赖,因此有必要在a的wather执行update操作时也更新b,也就意味着,a的watcher里需要收集着b的依赖。而收集的时机是执行b的回调时,this.a调用了a的get方法的时候
在computed部分,已经对计算属性的get方法进行了改写

function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      //调用一个计算属性的get方法时,会在watcher中收集依赖。
      watcher.depend() 
      return watcher.evaluate()
    }
  }

我们再修改一下wather代码:

class Watcher {
    constructors(component, getter, cb, options){
         this.cb = cb 
        this.getter = getter;
        this.id = UUID() 
        this.sync = options.sync; 
        this.vm = component; 
        if(options){
            this.computed = options.computed //由于是对计算属性特殊处理,那肯定要给个标识符以便判断
        }
        this.dirty = this.computed // for computed watchers
        this.value = this.lazy ? undefined : this.get();
    }
    
    update(){
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
    }
    
    run: function(){
         //拿到新值
        const value = this.get()
        if (value !== this.value || //基本类型的值直接比较
          // 对象没办法直接比较,因此都进行计算
          isObject(value)) {
          // set new value
          const oldValue = this.value
          this.value = value
          this.dirty = false
          cb.call(this.vm, value, oldValue)
        }
    }
    
    // 新增depend方法,收集计算属性的依赖
    depend () {
        if (this.dep && Dep.target) {
          this.dep.depend()
        }
      }
}
  
  //不要忘了还要返回当前computed的最新的值
  //由于可能不是立即更新的,因此根据dirty再判断一下,如果数据脏了,调用get再获取一下
  evaluate () {
    if (this.dirty) {
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }

在绑定依赖之前(computed的get被触发一次),computed用到的data数据改变是不会触发computed的重新计算的。

路径解析

对于render和computed想要收集依赖,我们只需要执行一遍回调函数就行,但是对于$watch方法,我们并不关心他的回调是什么,而更关心我们需要监听哪个值。
这里的需求多种多样,
比如单个值监听,监听对象的某个属性(.),比如多个值混合监听(&&, ||)等。这就需要对监听的路径进行解析。

 constructors(component, expOrFn, cb, options){
         this.cb = cb 
        this.id = UUID() 
        this.sync = options.sync; 
        this.vm = component; 
        if(options){
            this.computed = options.computed
        }
        if(typeof expOrFn === "function"){
            // render or computed
            this.getter = expOrFn 
        }else{
            this.getter = this.parsePath();
        }
        if(this.computed){
            this.value = undefined
            this.dep = new Dep() 
        }else{
            this.value = this.get(); //非计算属性是通过调用getter方法收集依赖。
        }
    }
    
    parsePath: function(){
        // 简单的路径解析,如果都是字符串则不需要解析
         if (/[^\w.$]/.test(path)) {
            return
          }
        // 这边只是简单解析了子属性的情况
          const segments = path.split('.')
          return function (obj) {
            for (let i = 0; i < segments.length; i++) {
              if (!obj) return
              obj = obj[segments[i]]
            }
            return obj
          }
    }

总结

我们在watcher乞丐版的基础上,根据实际需求推导出了更健全的watcher版本。下面是完整代码

class Watcher {
    constructors(component, getter, cb, options){
         this.cb = cb 
        this.getter = getter;
        this.id = UUID() 
        this.sync = options.sync; 
        this.vm = component; 
        if(options){
            this.computed = options.computed //由于是对计算属性特殊处理,那肯定要给个标识符以便判断
        }
        if(typeof expOrFn === "function"){
            // render or computed
            this.getter = expOrFn 
        }else{
            this.getter = this.parsePath();
        }
        this.dirty = this.computed // for computed watchers
        if(this.computed){
            // 对于计算属性computed而言,我们需要关心preValue吗?   *********************
            this.value = undefined
            // 如果是计算属性,就要收集依赖
            //同时根据按需加载的原则,这边不会手机依赖,主动执行回调函数。
            this.dep = new Dep() 
        }else{
            this.value = this.get(); //非计算属性是通过调用getter方法收集依赖。
        }
    }
    
    update(){
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
    }
    
    run: function(){
         //拿到新值
        const value = this.get()
        if (value !== this.value || //基本类型的值直接比较
          // 对象没办法直接比较,因此都进行计算
          isObject(value)) {
          // set new value
          const oldValue = this.value
          this.value = value
          this.dirty = false
          cb.call(this.vm, value, oldValue)
        }
    }

    
    // 新增depend方法,收集计算属性的依赖
    depend () {
        if (this.dep && Dep.target) {
          this.dep.depend()
        }
      }
}
  
  //不要忘了还要返回当前computed的最新的值
  //由于可能不是立即更新的,因此根据dirty再判断一下,如果数据脏了,调用get再获取一下
  evaluate () {
    if (this.dirty) {
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }

可以看到,基本vue的实现一样了。VUE中有些代码,比如teardown方法,清除自身的订阅信息我并没有加进来,因为没有想到合适的应用场景。
这种逆推的过程我觉得比直接读源码更有意思。直接读源码并不难,但很容易造成似是而非的情况。逻辑很容易理解,但是真正为什么这么写,一些细节原因很容易漏掉。但是不管什么框架都是为了解决实际问题的,从需求出发,才能更好的学习一个框架,并在自己的工作中加以借鉴。
借VUE的生命周期图进行展示

clipboard.png

局部图:

clipboard.png

从局部图里可以看出,vue收集依赖的入口只有两个,一个是在加载之前处理$wacth方法,一个是render生成虚拟dom。
而对于computed,只有在使用到时才会收集依赖。如果我们在watch和render中都没有使用,而是在methods中使用,那么加载的过程中是不会计算这个computed的,只有在调用methods中方法时才会计算。

查看原文

赞 1 收藏 1 评论 4

写意风流 评论了文章 · 2018-09-23

源码看React 事件机制

对React熟悉的同学都知道,React中的事件机制并不是原生的那一套,事件没有绑定在原生DOM上,发出的事件也是对原生事件的包装。
那么这一切是怎么实现的呢?

事件注册

首先还是看我们熟悉的代码

<button onClick={this.autoFocus}>点击聚焦</button>

这是我们在React中绑定事件的常规写法。经由JSX解析,button会被当做组件挂载。而onClick这时候也只是一个普通的props。
ReactDOMComponent在进行组件加载(mountComponent)、更新(updateComponent)的时候,需要对props进行处理(_updateDOMProperties):

ReactDOMComponent.Mixin = {
  _updateDOMProperties: function (lastProps, nextProps, transaction) {
    ...
    for (propKey in nextProps) {
      // 判断是否为事件属性
      if (registrationNameModules.hasOwnProperty(propKey)) {
        enqueuePutListener(this, propKey, nextProp, transaction);
      }
    }
  }
}
//这里进行事件绑定
function enqueuePutListener(inst, registrationName, listener, transaction) {
  ...
  //注意这里!!!!!!!!!
  //这里获取了当前组件(其实这时候就是button)所在的document
  var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
  listenTo(registrationName, doc);
  transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,
    registrationName: registrationName,
    listener: listener
  });
  function putListener() {
    var listenerToPut = this;
    EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
  }
}

绑定的重点是这里的listenTo方法。看源码(ReactBrowerEventEmitter)

//registrationName:需要绑定的事件
//当前component所属的document,即事件需要绑定的位置
listenTo: function (registrationName, contentDocumentHandle) {
    var mountAt = contentDocumentHandle;
    //获取当前document上已经绑定的事件
    var isListening = getListeningForDocument(mountAt);
    ...
      if (...) {
      //冒泡处理  
      ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(...);
      } else if (...) {
        //捕捉处理
        ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(...);
      }
      ...
  },

最后处理(EventListener的listen和capture中)

//eventType:事件类型,target: document对象,
//callback:是固定的,始终是ReactEventListener的dispatch方法
if (target.addEventListener) {
      target.addEventListener(eventType, callback, false);
      return {
        remove: function remove() {
          target.removeEventListener(eventType, callback, false);
        }
      };
    }

从事件注册的机制中不难看出:

  • 所有事件绑定在document上
  • 所以事件触发的都是ReactEventListener的dispatch方法

回调储存

看到这边你可能疑惑,所有回调都执行的ReactEventListener的dispatch方法,那我写的回调干嘛去了。别急,接着看:

function enqueuePutListener(inst, registrationName, listener, transaction) {
  ...
  //注意这里!!!!!!!!!
  //这里获取了当前组件(其实这时候就是button)所在的document
  var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
  //事件绑定
  listenTo(registrationName, doc);
 //这段代码表示将putListener放入回调序列,当组件挂载完成是会依次执行序列中的回调。putListener也是在那时候执行的。
 //不明白的可以看看本专栏中前两篇关于transaction和挂载机制的讲解
  transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,
    registrationName: registrationName,
    listener: listener
  });
  //保存回调
  function putListener() {
    var listenerToPut = this;
    EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
  }
}

还是这段代码,事件绑定我们介绍过,主要是listenTo方法。
当绑定完成以后会执行putListener。该方法会在ReactReconcileTransaction事务的close阶段执行,具体由EventPluginHub来进行管理

//
var listenerBank = {};
var getDictionaryKey = function (inst) {
//inst为组建的实例化对象
//_rootNodeID为组件的唯一标识
  return '.' + inst._rootNodeID;
}
var EventPluginHub = {
//inst为组建的实例化对象
//registrationName为事件名称
//listner为我们写的回调函数,也就是列子中的this.autoFocus
  putListener: function (inst, registrationName, listener) {
    ...
    var key = getDictionaryKey(inst);
    var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
    bankForRegistrationName[key] = listener;
    ...
  }
}

EventPluginHub在每个项目中只实例化一次。也就是说,项目组所有事件的回调都会储存在唯一的listenerBank中。

是不是有点晕,放上流程图,仔细回忆一下
图片描述

事件触发

注册事件时我们说过,所有的事件都是绑定在Document上。回调统一是ReactEventListener的dispatch方法。
由于冒泡机制,无论我们点击哪个DOM,最后都是由document响应(因为其他DOM根本没有事件监听)。也即是说都会触发dispatch

dispatchEvent: function(topLevelType, nativeEvent) {
    //实际触发事件的DOM对象
    var nativeEventTarget = getEventTarget(nativeEvent);
    //nativeEventTarget对应的virtual DOM
    var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(
      nativeEventTarget,
    );
    ...
    //创建bookKeeping实例,为handleTopLevelImpl回调函数传递事件名和原生事件对象
    //其实就是把三个参数封装成一个对象
    var bookKeeping = getTopLevelCallbackBookKeeping(
      topLevelType,
      nativeEvent,
      targetInst,
    );

    try {
    //这里开启一个transactIon,perform中执行了
    //handleTopLevelImpl(bookKeeping)
      ReactGenericBatching.batchedUpdates(handleTopLevelImpl, bookKeeping);
    } finally {
      releaseTopLevelCallbackBookKeeping(bookKeeping);
    }
  },

这里把节奏放慢点,我们一步步跟。

function handleTopLevelImpl(bookKeeping) {
//触发事件的真实DOM
  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
  //nativeEventTarget对应的ReactElement
  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);
  //bookKeeping.ancestors保存的是组件。
  var ancestor = targetInst;
  do {
    bookKeeping.ancestors.push(ancestor);
    ancestor = ancestor && findParent(ancestor);
  } while (ancestor);

  for (var i = 0; i < bookKeeping.ancestors.length; i++) {
    targetInst = bookKeeping.ancestors[i];
    //具体处理逻辑
    ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
  }
}
//这就是核心的处理了
handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
//首先封装event事件
    var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
    //发送包装好的event
    runEventQueueInBatch(events);
  }

事件封装

首先是EventPluginHubextractEvents

extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
    var events;
    var plugins = EventPluginRegistry.plugins;
    for (var i = 0; i < plugins.length; i++) {
      // Not every plugin in the ordering may be loaded at runtime.
      var possiblePlugin = plugins[i];
      if (possiblePlugin) {
      //主要看这边
        var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
        ......
      }
    }
    return events;
  },

接着看SimpleEventPlugin的方法

extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
    ......
    //这里是对事件的封装,但是不是我们关注的重点
    var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
    //重点看这边
    EventPropagators.accumulateTwoPhaseDispatches(event);
    return event;
}

接下来是方法中的各种引用,跳啊跳,转啊转,我们来到了ReactDOMTraversal中的traverseTwoPhase方法

//inst是触发事件的target的ReactElement
//fn:EventPropagator的accumulateDirectionalDispatches
//arg: 就是之前部分封装好的event(之所以说是部分,是因为现在也是在处理Event,这边处理完才是封装完成)
function traverseTwoPhase(inst, fn, arg) {
  var path = [];
  while (inst) {
   //注意path,这里以ReactElement的形式冒泡着,
   //把触发事件的父节点依次保存下来
    path.push(inst);
    //获取父节点
    inst = inst._hostParent;
  }
  var i;
  //捕捉,依次处理
  for (i = path.length; i-- > 0;) {
    fn(path[i], 'captured', arg);
  }
  //冒泡,依次处理
  for (i = 0; i < path.length; i++) {
    fn(path[i], 'bubbled', arg);
  }
}
//判断父组件是否保存了这一类事件
function accumulateDirectionalDispatches(inst, phase, event) {
//获取到回调
  var listener = listenerAtPhase(inst, event, phase);
  if (listener) {
  //如果有回调,就把包含该类型事件监听的DOM与对应的回调保存进Event。
  //accumulateInto可以理解成_.assign
  //记住这两个属性,很重要。
    event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
  }
}

listenerAtPhase里面执行的是EventPluginHub的getListener函数

getListener: function (inst, registrationName) {
    //还记得之前保存回调的listenerBank吧?
    var bankForRegistrationName = listenerBank[registrationName];
    if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) {
      return null;
    }
    //获取inst的_rootNodeId
    var key = getDictionaryKey(inst);
    //获取对应的回调
    return bankForRegistrationName && bankForRegistrationName[key];
  },

可以发现,React在分装原生nativeEvent时

  • 将有eventType属性的ReactElement放入 event._dispatchInstances
  • 将对应的回调依次放入event._dispatchListeners

事件分发

runEventQueueInBatch主要进行了两步操作

function runEventQueueInBatch(events) {
//将event事件加入processEventQueue序列
  EventPluginHub.enqueueEvents(events);
  //前一步保存好的processEventQueue依次执行
//executeDispatchesAndRelease
  EventPluginHub.processEventQueue(false);
}

  processEventQueue: function (simulated) {
    var processingEventQueue = eventQueue;
    eventQueue = null;
    if (simulated) {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
    } else {
    //重点看这里
    //forEachAccumulated可以看成forEach的封装
    //那么这里就是processingEventQueue保存的event依次执行executeDispatchesAndReleaseTopLevel(event)
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
    }
  },

executeDispatchesAndReleaseTopLevel(event)又是各种函数包装,最后干活的是

function executeDispatchesInOrder(event, simulated) {
  //对应的回调函数数组
  var dispatchListeners = event._dispatchListeners;
  //有eventType属性的ReactElement数组
  var dispatchInstances = event._dispatchInstances;
  
  ......
  
  if (Array.isArray(dispatchListeners)) {
    for (var i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) {
        break;
      }
      executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);
    }
  } else if (dispatchListeners) {
    executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

OK,这里总算出现了老熟人,在封装nativeEvent时我们保存在event里的两个属性,dispatchListenersdispatchInstances,在这里起作用。
代码很简单,如果有处理这个事件的回调函数,就一次进行处理。细节我们稍后讨论,先看看这里是怎么处理的吧

function executeDispatch(event, simulated, listener, inst) {
//type是事件类型
  var type = event.type || 'unknown-event';
  //这是触发事件的真实DOM,也就是列子中的button
  event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
  if (simulated) {
    ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
  } else {
  //看这里看这里
    ReactErrorUtils.invokeGuardedCallback(type, listener, event);
  }
  event.currentTarget = null;
}

终于来到最后了,代码位于ReactErrorUtil中
(为了帮助开发,React通过模拟真正的浏览器事件来获得更好的devtools集成。这段代码在开发模式下运行)

    //创造一个临时DOM
    var fakeNode = document.createElement('react');
    ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {
    //绑定回调函数的上下文
      var boundFunc = func.bind(null, a);
      //定义事件类型
      var evtType = 'react-' + name;
      //绑定事件
      fakeNode.addEventListener(evtType, boundFunc, false);
      //生成原生事件
      var evt = document.createEvent('Event');
      //将原生事件处理成我们需要的类型
      evt.initEvent(evtType, false, false);
      //发布事件---这里会执行回调
      fakeNode.dispatchEvent(evt);
      //移出事件监听
      fakeNode.removeEventListener(evtType, boundFunc, false);
    };

总体流程

图片描述

不难发现,我们经历了从真实DOM到Virtual DOM的来回转化。

常见问题的答案。

  1. e.stopPropagation不能阻止原生事件冒泡
    event是封装好的事件。他是在document的回调里进行封装,并执行回调的。而原生的监听,在document接收到冒泡时早就执行完了。
  2. e.nativeEvent.stopPropagation,回调无法执行。
    很简单,因为冒泡是从里到外,执行了原生的阻止冒泡,document当如捕捉不到,document都没捕捉到,React还玩个球啊,要知道,一切操作都放在docuemnt的回调里了。
  3. 怎么避免两者影响

    这个答案大家说了很多次,避免原生事件与React事件混用,或者通过target进行判断。
    

为什么这么设计

在网上看过一个列子说得很好,
一个Ul下面有1000个li标签。想在想为每个li都绑定一个事件,怎么操作?
总不可能一个个绑定吧?
其实这个和jquery绑定事件差不多。通过最外层绑定事件,当操作是点击任何一个li自然会冒泡到最外面的Ul,又可以通过最外面的target获取到具体操作的DOM。一次绑定,收益一群啊。

查看原文

写意风流 回答了问题 · 2018-09-23

解决Vue watcher非同步更新的问题

自问自答一下。这个问题其实想说清楚也很简单。之所以要以队列缓存起来,就是为了避免不必要更新。如,

data: {
    a: 1
},
computed: {
    b: function(){
        this.a + 1
    }
}

methods: {
    act: function(){
        this.a = 2;
        // do someting
        this.a = 1
    }
}

在act操作中,我们先改变a,再把它变回来。我们理想状况下是a没变,b也不重新计算。其实想想就知道这是做不到的。因为只要a改变了b的wather就已经在更新队列中了。我们能避免的,只是b的改变已经的其他计算。
这就要求,b的wather执行update的时候要拿到a最新的值来计算。这里就是1。
回到问题中来,如果队列中a的watehr已经更新过,那么就应该把后面的a的wather放到当前更新的wather后面,立即更新。这样可以保证后面的wather用到a是可以拿到最新的值。
同理,如果a的wather还没有更新,那么把新的a的wather放的之前的a的wather的下一位,也是为了保证后面的wather用到a是可以拿到最新的值

关注 2 回答 3

写意风流 关注了问题 · 2018-09-20

解决Vue watcher非同步更新的问题

问题描述

Vue中,如果wather不是同步更新的话,会放到一个队列中,同一事件循环结束后再更新。相关代码再scheduler的queueWatcher方法中。
这里面处理一个场景:
当这个队列正在更新时,又触发了wather。

  if (!flushing) {
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
  }

不明白为什么要这么处理。直接push到最后不行吗?
现在这样的设计又有什么好处?

追加描述,
vue这段代码也并不是同一个时间循环中,还没执行这个wather,再次触发这个wather就只执行一次。

     queue.splice(i + 1, 0, watcher)

这边是知道这个wather之前的index,并把第二次触发放到这个后面。也还是执行两次update。既然都是执行两次,为什么不直接push到最后

关注 2 回答 3

写意风流 提出了问题 · 2018-09-20

解决Vue watcher非同步更新的问题

问题描述

Vue中,如果wather不是同步更新的话,会放到一个队列中,同一事件循环结束后再更新。相关代码再scheduler的queueWatcher方法中。
这里面处理一个场景:
当这个队列正在更新时,又触发了wather。

  if (!flushing) {
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
  }

不明白为什么要这么处理。直接push到最后不行吗?
现在这样的设计又有什么好处?

追加描述,
vue这段代码也并不是同一个时间循环中,还没执行这个wather,再次触发这个wather就只执行一次。

     queue.splice(i + 1, 0, watcher)

这边是知道这个wather之前的index,并把第二次触发放到这个后面。也还是执行两次update。既然都是执行两次,为什么不直接push到最后

关注 2 回答 3

认证与成就

  • 获得 40 次点赞
  • 获得 7 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 7 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-09-05
个人主页被 558 人浏览