幽_月

幽_月 查看完整档案

深圳编辑北京邮电大学  |  电子 编辑腾讯  |  IMWEB团队前端工程师 编辑填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

幽_月 赞了文章 · 2019-08-16

webpack 代码拆分

如果利用 webpack 将项目中的所有代码打包在一起,很多时候是不适用的,因为代码中有些东西我们总是希望将其拆分出来。比如:

  • 样式表,希望利用 link 标签引入

  • 使用概率较低的模块,希望后期需要的时候异步加载

  • 框架代码,希望能利用浏览器缓存下部分不易变动的代码

下面是我在阅读 webpack 的官方文档时候,记录的一些笔记,部分地方使用了自己的话来讲,力图让它显得更易懂。

按需加载拆分

webpack 可以帮助我们将代码分成不同的逻辑块,在需要的时候加载这些代码。

使用 require.ensure() 来拆分代码

require.ensure() 是一种使用 CommonJS 的形式来异步加载模块的策略。在代码中通过 require.ensure([<fileurl>]) 引用模块,其使用方法如下:

require.ensure(dependencies: String[], callback: function(require), chunkName: String)

第一个参数指定依赖的模块,第二个参数是一个函数,在这个函数里面你可以使用 require 来加载其他的模块,webpack 会收集 ensure 中的依赖,将其打包在一个单独的文件中,在后续用到的时候使用 jsonp 异步地加载进去。

require.ensure(['./a'], function(require){
    let b = require('./b');
    let a = require('./a');
    console.log(a+b)
});

以上代码,a 和 b 会被打包在一起,在代码中执行到这段代码的时候,会异步地去加载,加载完成后执行函数里面的逻辑。

let a = require('./a');
require.ensure(['./a'], function(require){
    let b = require('./b');
    console.log(a+b)
});

如果这样写,那么 a 不会和 b 打包在一起,因为 a 已经被打包在主代码中了。

require.ensure(['./c'], function(require){
    let a = require('./a');
    console.log(a)
});

require.ensure(['./c'], function(require){
    let b = require('./b');
    console.log(b)
});

以上代码中两个模块都依赖了 c 模块,这个时候会拆分出两个模块,其中都包含了 c 模块,因为在实际运用中,以上两个 require.ensure 的执行顺序不确定,执行与否也不确定,因此需要将 c 模块都打包进去。

require.ensure 还可以传入第三个参数,这个参数用来指定打包的包名,对于上面这种情况,c 模块被打包入了两个包中,如果事先明确这两个包都会被使用,那么不妨将这两个包合并为一个,这样就不会有 c 模块被打包两次的问题了,所以可以将 chunkName 指定为同一个名字。

require.ensure(['./c'], function(require){
    let a = require('./a');
    console.log(a)
}, 'd');

require.ensure(['./c'], function(require){
    let b = require('./b');
    console.log(b)
}, 'd');

ok,这样以上两个 require.ensure 拆出来的包就合并为同一个了。

CSS 拆分

开发者,可能希望能将工程中的所有引入的 CSS 拆分为单个文件,这样可以利用缓存,且利用 CSS 和 JavaScript 并行加载,来加速 web 应用。

使用 css-loader

为了加载 css,这里需要用到 css-loader,配置方法如下:

module: {
    loaders: [{
        test: /\.css$/,
        exclude: /node_modules/,
        loader: 'css-loader'
    }]
}

这样在代码中就可以写如下代码:

let css = require('./main.css');
console.log('' + css);

通过 require 一个 css 得到其内容,当然了这里 require('./main.css') 实际得到的是一个对象,需要调用其 toString 方法将其转换为字符串。在代码中引用一段 css,这常常不是我们想要的。为此可以使用 style-loader 在代码执行起来的时候,会将这些 css 插入到 style 标签中,只是这里 css 还是存在于 js 中的,是后来动态插入到页面中的:

module: {
    loaders: [{
        test: /\.css$/,
        exclude: /node_modules/,
        loader: 'style-loader!css-loader'
    }]
}

更多时候,是希望将 css 拆分为单个文件,然后使用 link 标签嵌入到 html 中,CSS 和 JavaScript 可以并行加载,css 还可以被缓存下来。

使用 extract-text-webpack-plugin 来拆分 css

为了使用这个插件首先需要通过 npm 来安装它:

npm i --save-dev extract-text-webpack-plugin

然后在 webpack 的配置文件中使用该插件:

var ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = function () {
    return {
        entry: './index.js',
        output: {
            path: './build',
            filename: 'bundle.js'
        },
        module: {
            loaders: [{
                test: /\.css$/,
                exclude: /node_modules/,
                // 在 loader 中使用该插件
                loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
            }]
        },
        plugins: [
            // 将其添加在插件中
            new ExtractTextPlugin({ filename: 'bundle.css', disable: false, allChunks: true })
        ]
    }
}

需要注意的是,对于 webpack1 和 webpack2 这个插件的配置方法是不同的,差别比较细微,详情请看官方文档 extract text plugin for webpack 2

拆分业务代码与框架代码

通常一个 web 应用都会引用若干第三方库,这些第三方库通常比较稳定不会经常变动,但是如果将业务代码和框架代码打包在了一起,这样业务代码每次变动打包得到的结果都会变动,及时只改变了一个字符,浏览器也无法利用缓存,必须全部重新加载。因此,何不将第三方库单独打包在一起呢?

这里举个案例,一个 react 项目中使用了 reactreact-dom 这两个包,我希望将他们打包在一起,将业务代码打包在一起。

下面一步一步来:

1. 安装 reactreact-dom:

npm i react react-dom --save

2. 配置 entry,output 和 loader

先使用单入口,让代码工作起来。另外因为使用了 react 所以要使用 babel-loader 来加载 js

// webpack.config.js

module.exports = {
    entry: 'index.js',
    output: {
        path: 'build/',
        filname: '[name]@[chunkhash].js'
    },
    module:{
        loaders:[{
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel'
        }]
    }
}

3. 编写业务代码

index.js:

import React from 'react';
import ReactDOM from 'react-dom';


var Hello = React.createClass({
    render: function() {
        return <div>Hello {this.props.name}</div>;
    }
});

ReactDOM.render(<Hello name={'world'} />, document.getElementById('app'));

index.html:

<div id="app"></div>
<!--entry 为一个字符串,这个 chunk 的名字会是 main, 因此这里引入 main.js -->
<script data-original="build/main.js"></script>

启动 webpack-dev-server,打开浏览器这个时候应该能在页面上看到 hello world,这说明工作正常。

4. 拆分框架代码

为了拆分框架代码,我们需要增加一个入口,在这个入口中要包含 reactreact-dom

module.exports = {
    entry: {
        main: 'index.js',
        vendor: ['react', 'react-dom']
    }
    //...
}

单单像上面这样配置,打包后会得到 main.jsvendor.js,但会发现在 main.js 中依然包含了 react 和 react-dom 的代码,这是因为指定了入口后,webpack 就会从入口文件开始讲整个依赖打包进来,index.js 中引用了 react 和 react-dom 自然会被打包进去。要想达到之前所说的那个效果,还需要借助一个插件 —— CommonsChunkPlugin

5. 使用 CommonsChunkPlugin

这个插件的功能是将多个打包结果中公共的部分抽取出来,作为一个单独的文件。它符合目前的场景,因为 main.jsvendor.js 中存在一份公共的代码,那就是 vendor.js 中的内容。(这个说法并不准确,这里只是指 react 和 react-dom 都被打包进了这两个文件)

let webpack = require('webpack');

module.exports = {
    entry: {
        main: 'index.js',
        vendor: ['react', 'react-dom']
    },
    //...

    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor' // 指定一个希望作为公共包的入口
        })
    ]
}

再进行打包,这个时候会发现 main.js 中的代码不在包含 react 的代码了。看似实现了我们的需求,但真实应用下并没有这么简单,在实际项目中 js 脚本通常都会给添加一个 MD5 的 hash 在后面,形如 app@709d9850745a4c8ba1d4.js 这样每次打包后,如果文件内容变了,后面的 hash 也会变动。就以上场景,会发现当我们修改了业务代码后,得到的 hash 是不同的,因此每次都会得到两个不同的打包结果。业务代码改变了,拆分出来的框架包也变了,这显然不符合初衷 —— 利用浏览器缓存。

这是因为 webpack 在打包的时候会产生一些运行时代码,比如 __webpack_require__webpackJsonp 等等,这些函数是用来帮助 webpack 完成模块加载等功能的,业务代码的改变会导致业务代码打包后的 hash 值改变,而在 webpack 的运行时代码中实际上是保存了打包后的结果的文件名的,因为它在异步加载模块的时候需要用到。因此,下面需要做的是将 webpack 的运行时代码拆分出来。

修改 plugins 如下,将 name 修改为 names,并增加一个 init 的包名,执行打包,会发现 webpack 的运行时代码都被入该包内。

plugins: [
    new webpack.optimize.CommonsChunkPlugin({
        names: ['vendor', 'init']
    })
]

这样以来,修改了业务代码后,vendor 因为只引用了 react 和 react-dom 因此,业务代码的改变不会改变 vendor 这个包的内容,hash 也保持不变。但,也仅仅如此 如果你引用了其他模块,webpack 收集依赖的时候会给每个模块编一个号,引入其他模块会导致模块数改变,也就会导致编号改变,这个时候打包出来的 vendor 还是会改变。

那么到底该如何解决这个问题呢?在官方文档上没有找到解决方案。后面我会继续探索这一问题,找到解决方案后会及时更新到这里,如果你有解决方案,还请不吝赐教,谢谢。

查看原文

赞 7 收藏 41 评论 3

幽_月 赞了文章 · 2019-03-23

ES6 Class 继承与 super

原文 https://javascript.info/class...

Class 继承与 super

class 可以 extends 自另一个 class。这是一个不错的语法,技术上基于原型继承。

要继承一个对象,需要在 {..} 前指定 extends 和父对象。

这个 Rabbit 继承自 Animal

class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  run(speed) {
    this.speed += speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }

  stop() {
    this.speed = 0;
    alert(`${this.name} stopped.`);
  }

}


// Inherit from Animal
class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }
}


let rabbit = new Rabbit("White Rabbit");

rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.hide(); // White Rabbit hides!

如你所见,如你所想,extend 关键字实际上是在 Rabbit.prototype 添加 [Prototype]],引用到 Animal.prototype

所以现在 rabbit 既可以访问它自己的方法,也可以访问 Animal 的方法。

extends 后可跟表达式

Class 语法的 `extends' 后接的不限于指定一个类,更可以是表达式。

例如一个生成父类的函数:

function f(phrase) {
  return class {
    sayHi() { alert(phrase) }
  }
}


class User extends f("Hello") {}


new User().sayHi(); // Hello

例子中,class User 继承了 f('Hello')返回的结果。

对于高级编程模式,当我们使用的类是根据许多条件使用函数来生成时,这就很有用。

重写一个方法

现在让我们进入下一步,重写一个方法。到目前为止,RabbitAnimal 继承了 stop 方法,this.speed = 0

如果我们在 Rabbit 中指定了自己的 stop,那么会被优先使用:

class Rabbit extends Animal {
  stop() {
    // ...this will be used for rabbit.stop()
  }
}

......但通常我们不想完全替代父方法,而是在父方法的基础上调整或扩展其功能。我们进行一些操作,让它之前/之后或在过程中调用父方法。

Class 为此提供 super关键字。

  • 使用 super.method(...) 调用父方法。
  • 使用 super(...) 调用父构造函数(仅在 constructor 函数中)。

例如,让兔子在 stop 时自动隐藏:

class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  run(speed) {
    this.speed += speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }

  stop() {
    this.speed = 0;
    alert(`${this.name} stopped.`);
  }

}

class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }


  stop() {
    super.stop(); // call parent stop
    this.hide(); // and then hide
  }

}

let rabbit = new Rabbit("White Rabbit");

rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.stop(); // White Rabbit stopped. White rabbit hides!

现在,Rabbitstop 方法通过 super.stop() 调用父类的方法。

箭头函数无 super

正如在 arrow-functions 一章中提到,箭头函数没有 super

它会从外部函数中获取 super。例如:

class Rabbit extends Animal {
  stop() {
    setTimeout(() => super.stop(), 1000); // call parent stop after 1sec
  }
}

箭头函数中的 superstop() 中的相同,所以它按预期工作。如果我们在这里用普通函数,便会报错:

// Unexpected super
setTimeout(function() { super.stop() }, 1000);

重写构造函数

对于构造函数来说,这有点棘手 tricky。

直到现在,Rabbit 都没有自己的 constructor
Till now, Rabbit did not have its own constructor.

根据规范,如果一个类扩展了另一个类并且没有 constructor ,那么会自动生成如下 constructor

class Rabbit extends Animal {
  // generated for extending classes without own constructors

  constructor(...args) {
    super(...args);
  }

}

我们可以看到,它调用了父 constructor 传递所有参数。如果我们不自己写构造函数,就会发生这种情况。

现在我们将一个自定义构造函数添加到 Rabbit 中。除了name,我们还会设置 earLength

class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  // ...
}

class Rabbit extends Animal {


  constructor(name, earLength) {
    this.speed = 0;
    this.name = name;
    this.earLength = earLength;
  }


  // ...
}


// Doesn't work!
let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.

哎呦出错了!现在我们不能生成兔子了,为什么呢?

简单来说:继承类中的构造函数必须调用 super(...),(!)并且在使用 this 之前执行它。

...但为什么?这是什么情况?嗯...这个要求看起来确实奇怪。

现在我们探讨细节,让你真正理解其中缘由 ——

在JavaScript中,继承了其他类的构造函数比较特殊。在继承类中,相应的构造函数被标记为特殊的内部属性 [[ConstructorKind]]:“derived”

区别在于:

  • 当一个普通的构造函数运行时,它会创建一个空对象作为 this,然后继续运行。
  • 但是当派生的构造函数运行时,与上面说的不同,它指望父构造函数来完成这项工作。

所以如果我们正在构造我们自己的构造函数,那么我们必须调用 super,否则具有 this 的对象将不被创建,并报错。

对于 Rabbit 来说,我们需要在使用 this 之前调用 super(),如下所示:

class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  // ...
}

class Rabbit extends Animal {

  constructor(name, earLength) {

    super(name);

    this.earLength = earLength;
  }

  // ...
}


// now fine
let rabbit = new Rabbit("White Rabbit", 10);
alert(rabbit.name); // White Rabbit
alert(rabbit.earLength); // 10

Super 的实现与 [[HomeObject]]

让我们再深入理解 super 的底层实现,我们会看到一些有趣的事情。

首先要说的是,以我们迄今为止学到的知识来看,实现 super 是不可能的。

那么思考一下,这是什么原理?当一个对象方法运行时,它将当前对象作为 this。如果我们调用 super.method(),那么如何检索 method?很容易想到,我们需要从当前对象的原型中取出 method。从技术上讲,我们(或JavaScript引擎)可以做到这一点吗?

也许我们可以从 this 的 [[Prototype]] 中获得方法,就像 this .__ proto __.method 一样?不幸的是,这是行不通的。

让我们试一试,简单起见,我们不使用 class 了,直接使用普通对象。

在这里,rabbit.eat() 调用父对象的 animal.eat() 方法:

let animal = {
  name: "Animal",
  eat() {
    alert(`${this.name} eats.`);
  }
};

let rabbit = {
  __proto__: animal,
  name: "Rabbit",
  eat() {

    // that's how super.eat() could presumably work
    this.__proto__.eat.call(this); // (*)

  }
};

rabbit.eat(); // Rabbit eats.

(*) 这一行,我们从原型(animal)中取出 eat,并以当前对象的上下文中调用它。请注意,.call(this) 在这里很重要,因为只写 this .__ proto __.eat() 的话 eat 的调用对象将会是 animal,而不是当前对象。

以上代码的 alert 是正确的。

但是现在让我们再添加一个对象到原型链中,就要出事了:

let animal = {
  name: "Animal",
  eat() {
    alert(`${this.name} eats.`);
  }
};

let rabbit = {
  __proto__: animal,
  eat() {
    // ...bounce around rabbit-style and call parent (animal) method
    this.__proto__.eat.call(this); // (*)
  }
};

let longEar = {
  __proto__: rabbit,
  eat() {
    // ...do something with long ears and call parent (rabbit) method
    this.__proto__.eat.call(this); // (**)
  }
};


longEar.eat(); // Error: Maximum call stack size exceeded

噢,完蛋!调用 longEar.eat() 报错了!

这原因一眼可能看不透,但如果我们跟踪 longEar.eat() 调用,大概就知道为什么了。在 (*)(**) 两行中, this 的值是当前对象(longEar)。重点来了:所有方法都将当前对象作为 this,而不是原型或其他东西。

因此,在两行 (*)(**) 中,this.__ proto__ 的值都是 rabbit。他们都调用了 rabbit.eat,于是就这么无限循环下去。

情况如图:

1.在 longEar.eat() 里面,(**) 行中调用了 rabbit.eat,并且this = longEar

// inside longEar.eat() we have this = longEar
this.__proto__.eat.call(this) // (**)
// becomes
longEar.__proto__.eat.call(this)
// that is
rabbit.eat.call(this);

2.然后在rabbit.eat(*) 行中,我们希望传到原型链的下一层,但是 this = longEar,所以 this .__ proto __.eat又是 rabbit.eat

// inside rabbit.eat() we also have this = longEar
this.__proto__.eat.call(this) // (*)
// becomes
longEar.__proto__.eat.call(this)
// or (again)
rabbit.eat.call(this);
  1. ...因此 rabbit.eat 在无尽循环调动,无法进入下一层。

这个问题不能简单使用 this 解决。

[[HomeObject]]

为了提供解决方案,JavaScript 为函数添加了一个特殊的内部属性:[[HomeObject]]

当函数被指定为类或对象方法时,其 [[HomeObject]] 属性为该对象。

这实际上违反了 unbind 函数的思想,因为方法记住了它们的对象。并且 [[HomeObject]] 不能被改变,所以这是永久 bind(绑定)。所以在 JavaScript 这是一个很大的变化。

但是这种改变是安全的。 [[HomeObject]] 仅用于在 super 中获取下一层原型。所以它不会破坏兼容性。

让我们来看看它是如何在 super 中运作的:

let animal = {
  name: "Animal",
  eat() {         // [[HomeObject]] == animal
    alert(`${this.name} eats.`);
  }
};

let rabbit = {
  __proto__: animal,
  name: "Rabbit",
  eat() {         // [[HomeObject]] == rabbit
    super.eat();
  }
};

let longEar = {
  __proto__: rabbit,
  name: "Long Ear",
  eat() {         // [[HomeObject]] == longEar
    super.eat();
  }
};


longEar.eat();  // Long Ear eats.

每个方法都会在内部 [[HomeObject]] 属性中记住它的对象。然后 super 使用它来解析原型。

在类和普通对象中定义的方法中都定义了 [[HomeObject]],但是对于对象,必须使用:method() 而不是 "method: function()"

在下面的例子中,使用非方法语法(non-method syntax)进行比较。这么做没有设置 [[HomeObject]] 属性,继承也不起作用:

let animal = {
  eat: function() { // should be the short syntax: eat() {...}
    // ...
  }
};

let rabbit = {
  __proto__: animal,
  eat: function() {
    super.eat();
  }
};


rabbit.eat();  // Error calling super (because there's no [[HomeObject]])

静态方法和继承

class 语法也支持静态属性的继承。

例如:

class Animal {

  constructor(name, speed) {
    this.speed = speed;
    this.name = name;
  }

  run(speed = 0) {
    this.speed += speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }

  static compare(animalA, animalB) {
    return animalA.speed - animalB.speed;
  }

}

// Inherit from Animal
class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }
}

let rabbits = [
  new Rabbit("White Rabbit", 10),
  new Rabbit("Black Rabbit", 5)
];

rabbits.sort(Rabbit.compare);

rabbits[0].run(); // Black Rabbit runs with speed 5.

现在我们可以调用 Rabbit.compare,假设继承的 Animal.compare 将被调用。

它是如何工作的?再次使用原型。正如你猜到的那样,extends 同样给 Rabbit 提供了引用到 Animal[Prototype]

所以,Rabbit 函数现在继承 Animal 函数。Animal 自带引用到 Function.prototype[[Prototype]](因为它不 extend 其他类)。

看看这里:

class Animal {}
class Rabbit extends Animal {}

// for static propertites and methods
alert(Rabbit.__proto__ === Animal); // true

// and the next step is Function.prototype
alert(Animal.__proto__ === Function.prototype); // true

// that's in addition to the "normal" prototype chain for object methods
alert(Rabbit.prototype.__proto__ === Animal.prototype);

这样 Rabbit 可以访问 Animal 的所有静态方法。

在内置对象中没有静态继承

请注意,内置类没有静态 [[Prototype]] 引用。例如,Object 具有 Object.definePropertyObject.keys等方法,但 ArrayDate 不会继承它们。

DateObject 的结构:

DateObject 之间毫无关联,他们独立存在,不过 Date.prototype 继承于 Object.prototype,仅此而已。

造成这个情况是因为 JavaScript 在设计初期没有考虑使用 class 语法和继承静态方法。

原生拓展

Array,Map 等内置类也可以扩展。

举个例子,PowerArray 继承自原生 Array

// add one more method to it (can do more)
class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

let filteredArr = arr.filter(item => item >= 10);
alert(filteredArr); // 10, 50
alert(filteredArr.isEmpty()); // false

请注意一件非常有趣的事情。像 filtermap 和其他内置方法 - 返回新的继承类型的对象。他们依靠 constructor 属性来做到这一点。

在上面的例子中,

arr.constructor === PowerArray

所以当调用 arr.filter() 时,它自动创建新的结果数组,就像 new PowerArray 一样,于是我们可以继续使用 PowerArray 的方法。

我们甚至可以自定义这种行为。如果存在静态 getter Symbol.species,返回新建对象使用的 constructor。

下面的例子中,由于 Symbol.species 的存在,mapfilter等内置方法将返回普通的数组:

class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }


  // built-in methods will use this as the constructor
  static get [Symbol.species]() {
    return Array;
  }

}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

// filter creates new array using arr.constructor[Symbol.species] as constructor
let filteredArr = arr.filter(item => item >= 10);


// filteredArr is not PowerArray, but Array

alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function

我们可以在其他 key 使用 Symbol.species,可以用于剥离结果值中的无用方法,或是增加其他方法。

查看原文

赞 56 收藏 38 评论 0

幽_月 评论了文章 · 2019-01-21

一看就晕的React事件机制

前言

上篇文章我们了解了React合成事件跟原生绑定事件是有区别的,本篇文章从源码来深挖一下React的事件机制。

TL;DR :

  • react事件机制分为两个部分:1、事件注册 2、事件分发
  • 事件注册部分,所有的事件都会注册到document上,拥有统一的回调函数dispatchEvent来执行事件分发
  • 事件分发部分,首先生成合成事件,注意同一种事件类型只能生成一个合成事件Event,如onclick这个类型的事件,dom上所有带有通过jsx绑定的onClick的回调函数都会按顺序(冒泡或者捕获)会放到Event._dispatchListeners 这个数组里,后面依次执行它。

还是使用上次的栗子:

class ExampleApplication extends React.Component {
    componentDidMount() {
        document.addEventListener('click', () => {
            alert('document click');
        })
    }

    outClick(e) {
        console.log(e.currentTarget);
        alert('outClick');
    }
    
    onClick(e) {
        console.log(e.currentTarget);
        alert('onClick');
        e.stopPropagation();
    }
    render() {
        return <div onClick={this.outClick}>
            <button onClick={this.onClick}> 测试click事件 </button>
        </div>
    }
}

分析源码之前,有些工作和知识要提前准备,普及一下:

  1. 请各位准备好一个编辑器,自行用react-starter-kit建一个react项目,复制上面的代码,渲染上面的组件,然后打开控制台
  2. 下图是整个事件机制的流程图,后面会分部分解析
    https://www.processon.com/dia...
  3. 普及几个功能函数,提前了解它的作用
// 作用:如果只是单个next,则直接返回,如果有数组,返回合成的数组,里面有个
//current.push.apply(current, next)可以学习一下,我查了一下[资料][3]https://jsperf.com/array-prototype-push-apply-vs-concat/2,这样组合数组比concat效率更高

// 栗子:input accumulateInto([],[])

function accumulateInto(current, next) {
  if (current == null) {
    return next;
  }

  // Both are not empty. Warning: Never call x.concat(y) when you are not
  // certain that x is an Array (x could be a string with concat method).
  if (Array.isArray(current)) {
    if (Array.isArray(next)) {
      current.push.apply(current, next);
      return current;
    }
    current.push(next);
    return current;
  }

  if (Array.isArray(next)) {
    // A bit too dangerous to mutate `next`.
    return [current].concat(next);
  }

  return [current, next];
}

// 这个其实就是用来执行函数的,当arr时数组的时候,arr里的每一个项都作为回调函数cb的参数执行;
// 如果不是数组,直接执行回调函数cb,参数为arr
// 例如:
// arr为数组:forEachAccumulated([1,2,3], (item) => {console.log(item), this})
// 此时会打印出 1,2,3
// arr不为数组,forEachAccumulated(1, (item) => {console.log(item), this})
// 此时会打印出 1

function forEachAccumulated(arr, cb, scope) {
  if (Array.isArray(arr)) {
    arr.forEach(cb, scope);
  } else if (arr) {
    cb.call(scope, arr);
  }
}

React事件机制

React事件机制分为两块:

  • 事件注册
  • 事件分发

我们一步步来看:

事件注册

整个过程从ReactDomComponent开始,重点在enqueuePutListener,这个函数做了三件事情,详细请参考下面源码:

ReactDomComponent.js

function enqueuePutListener () {
    // 省略部分代码
    ...
    // 1、*重要:在这里取出button所在的document*
      var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
    // 2、在document上注册事件,同一个事件类型只会被注册一次
      listenTo(registrationName, doc);
    // 3、mountReady之后将回调函数存在ListernBank中
      transaction.getReactMountReady().enqueue(putListener, {
        inst: inst,
        registrationName: registrationName,
        listener: listener
      });
}

接下来看看第二步:在document上注册事件 的过程,流程图如下:

接着我们抽出每个文件的重点函数出来分析:

ReactBrowserEventEmitter.js

listenTo: function (registrationName, contentDocumentHandle) {
    var mountAt = contentDocumentHandle;
    // 检测document上是否已经监听onClick事件,所以前面说同一类型事件只会绑定一次
    var isListening = getListeningForDocument(mountAt);
    // 获得dependency,将onClick 转成topClick,这只是一种处理方式不用纠结
    var dependencies = 
   EventPluginRegistry.registrationNameDependencies[registrationName];
   // 中间是对各种事件类型给document绑定捕获事件或者冒泡事件,大部分都是冒泡,
   ...
   // 这里我们的topClick,绑定的是冒泡事件
   else if (topEventMapping.hasOwnProperty(dependency)) {  
   // trapBubbledEvent会在下面分析
 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
   }
   // 最后把topClick标记为已注册过,防止重复注册
   isListening[dependency] = true;
}

由于onclick绑定的是冒泡事件,所以我们来看看trapBubbledEvent

ReactEventListener.js

// 输入: topClick, click, doc
trapBubbledEvent: function (topLevelType, handlerBaseName, element) {
    if (!element) {
      return null;
    }
    //  EventListener 要做的事情就是把事件绑定到document上,注意这里无论是注册冒泡还是捕获事件,最终的回调函数都是dispatchEvent
    
    return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
  },
  
// EventListener.js
// 输入doc, click, dispatchEvent
// 这个函数其实就是我们熟悉的兼容浏IE浏览器事件绑定的方法
listen: function listen(target, eventType, callback) {
    if (target.addEventListener) {
      target.addEventListener(eventType, callback, false);
      return {
        remove: function remove() {
          target.removeEventListener(eventType, callback, false);
        }
      };
    } else if (target.attachEvent) {
      target.attachEvent('on' + eventType, callback);
      return {
        remove: function remove() {
          target.detachEvent('on' + eventType, callback);
        }
      };
    }
  },

注意这里无论是注册冒泡还是捕获事件,最终的回调函数都是dispatchEvent,所以我们来看看dispatchEvent怎么处理事件分发。

dispatchEvent

看到这里大家会奇怪,所有的事件的回调函数都是dispatchEvent来处理,那事件onClick原来的回调函数存到哪里去了呢?

再回来看事件注册的第三步:mountReady之后将回调函数存在ListernBank中

ReactDomComponent.js

function enqueuePutListener () {
    // 省略部分代码
    ...
    // 1、*重要:在这里取出button所在的document*
      var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
    // 2、在document上注册事件,同一个事件类型只会被注册一次
      listenTo(registrationName, doc);
    // 3、mountReady之后将回调函数存在ListernBank中
      transaction.getReactMountReady().enqueue(putListener, {
        inst: inst,
        registrationName: registrationName,
        listener: listener
      });
}

document上注册完所有的事件之后,还需要把listener 放到listenerBank中以listenerBank[registrationName][key] 这样的形式存起来,然后在dispatchEvent里面使用。

将listener放到listenerBank中储存的过程如下:

ReactDomComponent.js

// 在putListener里存入listener
function putListener() {
  var listenerToPut = this;
  // 先put的是外层的listener - outClick,所以这里的inst是外层div
  // registrationName是onclick,listener是outClick
  EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
}

EventPluginHub.js

  /**
   * Stores `listener` at `listenerBank[registrationName][key]`. Is idempotent.
   *
   * @param {object} inst The instance, which is the source of events.
   * @param {string} registrationName Name of listener (e.g. `onClick`).
   * @param {function} listener The callback to store.
   */
  putListener: function (inst, registrationName, listener) {
    var key = getDictionaryKey(inst); // 先根据inst得到唯一的key
    var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
    // 可以看到最终listener 在 listenerBank里,最终以listenerBank[registrationName][key] 存在

    bankForRegistrationName[key] = listener; 
   
    var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
    if (PluginModule && PluginModule.didPutListener) {
      // 这里的didPutListener只是为了兼容手机safari对non-interactive元素
      // 双击响应不正确,详情可以参考这篇[文章][7]
      //https://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
      PluginModule.didPutListener(inst, registrationName, listener);
    }
  },

以上就是事件注册的过程,接下来在看dispatchEvent如何处理事件分发。

事件分发

在介绍事件分发之前,有必要先介绍一下生成合成事件的过程,链接是https://segmentfault.com/a/11...

了解合成事件生成的过程之后,我们需要get一个点:合成事件收集了一波同类型(例如click)的回调函数存在了合成事件event._dispatchListeners这个数组里,然后将它们事件对应的虚拟dom节点放到_dispatchInstances 就本例来说,_dispatchListeners= [onClick, outClick],之后在一起执行。

接下来看看事件分发的过程:

EventListener.js

dispatchEvent: function (topLevelType, nativeEvent) {
    if (!ReactEventListener._enabled) {
      return;
    }
    // 这里得到TopLevelCallbackBookKeeping的实例对象,本例中第一次触发dispatchEvent时
    // bookKeeping = {ancestors: [],nativeEvent,‘topClick’}
    var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
    try {
      // Event queue being processed in the same cycle allows
      // `preventDefault`.
      // 接着执行handleTopLevelImpl(bookKeeping)
      ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
    } finally {
      TopLevelCallbackBookKeeping.release(bookKeeping);
    }
  }

function handleTopLevelImpl(bookKeeping) {
  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
  // 获取当前事件的虚拟dom元素
  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);

  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];
    // 这里的_handleTopLevel 对应的就是ReactEventEmitterMixin.js里的handleTopLevel
    ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
  }
}

// 这里的findParent曾经给我带来误导,我以为去找当前元素所有的父节点,但其实不是的,
// 我们知道一般情况下,我们的组件最后会被包裹在<div id='root'></div>的标签里
// 一般是没有组件再去嵌套它的,所以通常返回null
/**
 * Find the deepest React component completely containing the root of the
 * passed-in instance (for use when entire React trees are nested within each
 * other). If React trees are not nested, returns null.
 */
function findParent(inst) {
  while (inst._hostParent) {
    inst = inst._hostParent;
  }
  var rootNode = ReactDOMComponentTree.getNodeFromInstance(inst);
  var container = rootNode.parentNode;
  return ReactDOMComponentTree.getClosestInstanceFromNode(container);
}

上面这段代码的重点就是_handleTopLevel,它可以获取合成事件,并且去执行它。

下面看看具体是如何执行:

ReactEventEmitterMixin.js

function runEventQueueInBatch(events) {
  // 1、先将事件放进队列里
  EventPluginHub.enqueueEvents(events);
  // 2、执行它
  EventPluginHub.processEventQueue(false);
}

var ReactEventEmitterMixin = {
  /**
   * Streams a fired top-level event to `EventPluginHub` where plugins have the
   * opportunity to create `ReactEvent`s to be dispatched.
   */
  handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
     // 用EventPluginHub生成合成事件
    var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
    //  执行合成事件
    runEventQueueInBatch(events);
  }
};

执行的过程分成两步:

  1. 将事件放进队列
  2. 执行

执行的细节如下:

EventPluginHub.js

  var executeDispatchesAndReleaseTopLevel = function (e) {
    return executeDispatchesAndRelease(e, false);
  };

  var executeDispatchesAndRelease = function (event, simulated) {
      if (event) {
         // 在这里dispatch事件
        EventPluginUtils.executeDispatchesInOrder(event, simulated);
         // 释放事件
        if (!event.isPersistent()) {
          event.constructor.release(event);
        }
      }
  };

  enqueueEvents: function (events) {
    if (events) {
      eventQueue = accumulateInto(eventQueue, events);
    }
  },

  /**
   * Dispatches all synthetic events on the event queue.
   *
   * @internal
   */
  processEventQueue: function (simulated) {
    // Set `eventQueue` to null before processing it so that we can tell if more
    // events get enqueued while processing.
    var processingEventQueue = eventQueue;
    eventQueue = null;
    if (simulated) {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
    } else {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
    }
    // This would be a good time to rethrow if any of the event fexers threw.
    ReactErrorUtils.rethrowCaughtError();
  },

上段代码里,我们最终会走到

forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);

forEachAccumulated这个函数我们之前讲过,就是对数组processingEventQueue的每一个合成事件都使用executeDispatchesAndReleaseTopLevel来dispatch 事件。

所以各位同学们,注意到这里我们已经走到最核心的部分,dispatch 合成事件了,下面看看dispatch的详细过程:

EventPluginUtils.js

/**
 * Standard/simple iteration through an event's collected dispatches.
 */
function executeDispatchesInOrder(event, simulated) {
  var dispatchListeners = event._dispatchListeners;
  var dispatchInstances = event._dispatchInstances;

  if (Array.isArray(dispatchListeners)) {
    for (var i = 0; i < dispatchListeners.length; i++) {
     // 由这里可以看出,合成事件的stopPropagation只能阻止react合成事件的冒泡,
     // 因为event._dispatchListeners 只记录了由jsx绑定的绑定的事件,对于原生绑定的是没有记录的
      if (event.isPropagationStopped()) {
        break;
      }
      // Listeners and Instances are two parallel arrays that are always in sync.
      executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);
    }
  } else if (dispatchListeners) {
    executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

由上面的函数可知,dispatch 合成事件分为两个步骤:

  1. 通过_dispatchListeners里得到所有绑定的回调函数,在通过_dispatchInstances的绑定回调函数的虚拟dom元素
  2. 循环执行_dispatchListeners里所有的回调函数,这里有一个特殊情况,也是react阻止冒泡的原理

当回调函数里使用了stopPropagation会使得数组后面的回调函数不能执行,这样就做到了阻止事件冒泡

目前还是还有看到执行事件的代码,在接着看:

EventPluginHub.js

function executeDispatch(event, simulated, listener, inst) {
  var type = event.type || 'unknown-event';
  // 注意这里将事件对应的dom元素绑定到了currentTarget上
  event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
  if (simulated) {
    ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
  } else {
    // 一般都是非模拟的情况,执行invokeGuardedCallback
    ReactErrorUtils.invokeGuardedCallback(type, listener, event);
  }
  event.currentTarget = null;
}

上面这个函数最重要的功能就是将事件对应的dom元素绑定到了currentTarget上,
这样我们通过e.currentTarget就可以找到绑定事件的原生dom元素。

下面就是整个执行过程的尾声了:

ReactErrorUtils.js

var fakeNode = document.createElement('react');
ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {
  var boundFunc = function () {
    func(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);
};

invokeGuardedCallback可知,最后react调用了faked元素的dispatchEvent方法来触发事件,并且触发完毕之后立即移除监听事件。

总的来说,整个click事件被分发的过程就是:
1、用EventPluginHub生成合成事件,这里注意同一事件类型只会生成一个合成事件,里面的_dispatchListeners里储存了同一事件类型的所有回调函数

2、按顺序去执行它

就辣么简单!

本文比较长,有不理解的欢迎提问~ 或者有理解错误的也请大家指正。
最后附上整个流程图文件:

clipboard.png

s://segmentfault.com/a/1190000013343819

查看原文

幽_月 评论了文章 · 2019-01-17

诡异!React stopPropagation失灵

前言

先来看这
故事的开头是这样的:写代码的过程中,发现在内层用stopPropagation阻止绑定在document上的事件的时候,是没办法做到的,只可以阻止outClick事件的触发。

class ExampleApplication extends React.Component {
    componentDidMount() {
        document.addEventListener('click', () => {
            alert('document click');
        })
    }

    outClick(e) {
        console.log(e.currentTarget);
        alert('outClick');
    }
    
    onClick(e) {
        console.log(e.currentTarget);
        alert('onClick');
        e.stopPropagation();
    }
    render() {
        return <div onClick={this.outClick}>
            <button onClick={this.onClick}> 测试click事件 </button>
        </div>
    }
}

关于这个问题的解释,网上五花八门,有些将其归结为是由于事件委托的原因,例如这篇文章里说

我们直接在jsx模板上绑定的事件,都是委托在了document上,那自然要比直接在dom上绑定的事件慢了,等document收到事件后才去e.stopPropagation(),太晚了

其实从上面例子的输出: 由'onClick',再到'document click'可知,其实原生document上绑定的事件时最后执行的,所以并不是因为document收到事件快慢的原因而导致这个问题。

真相

真相只有一个,那就是:
出现上述bug的主要原因是混用浏览器原生事件跟React合成事件

详细解释:React有自己的一套事件处理机制,它会将所有的事件都绑定在document上,然后再用dispatchEvent统一分发,这时候分发的是合成事件
onClick(e)这时候拿到的e其实是合成事件,只能阻止合成事件的冒泡。

举个栗子来验证一下


class ExampleApplication extends React.Component {
    componentDidMount() {
        document.addEventListener('click', () => {
            alert('document click');
        })
        document.getElementById('div1').addEventListener('click', () => {
            alert('原生outClick');
        })
    }

    outClick(e) {
        console.log(e.currentTarget);
        alert('合成outClick');
    }
    
    onClick(e) {
        console.log(e.currentTarget);
        alert('onClick');
        e.stopPropagation();
    }
    render() {
        return <div id="div1" onClick={this.outClick}>
            <button onClick={this.onClick}> 测试click事件 </button>
        </div>
    }
}

做了点小改动,就是外层的div绑定的函数用原生的方式跟jsx的方式都绑定一次,所以最终的输出为

'原生outClick', 'onClick','document click'

所以button回调函数里的stopPropagation只能阻止合成事件的冒泡,而对于原生绑定的,则不行。

解决方法

解决方法有几种,我个人认为最简单的就是直接在onClick里利用[event.stopImmediatePropagation()][2]。原理是这样的:

对于例子一里,document其实绑定了两个事件:

// react 合成事件, dispatchEvent里面执行回调函数
document.addEventListener('click', dispatchEvent);

// 浏览器原生
document.addEventListener('click', () => {
   alert('document click');
})

dispatchEvent里的stopImmediatePropagation可以使得绑定在document上的其他事件就不会被触发

    onClick(e) {
        console.log(e.currentTarget);
        alert('onClick');
        e.nativeEvent.stopImmediatePropagation();
    }

当然本篇文章只是为了抛砖引玉,重点是下一篇的react事件机制,欢迎继续收看。

查看原文

幽_月 评论了文章 · 2019-01-17

一看就晕的React事件机制

前言

上篇文章我们了解了React合成事件跟原生绑定事件是有区别的,本篇文章从源码来深挖一下React的事件机制。

TL;DR :

  • react事件机制分为两个部分:1、事件注册 2、事件分发
  • 事件注册部分,所有的事件都会注册到document上,拥有统一的回调函数dispatchEvent来执行事件分发
  • 事件分发部分,首先生成合成事件,注意同一种事件类型只能生成一个合成事件Event,如onclick这个类型的事件,dom上所有带有通过jsx绑定的onClick的回调函数都会按顺序(冒泡或者捕获)会放到Event._dispatchListeners 这个数组里,后面依次执行它。

还是使用上次的栗子:

class ExampleApplication extends React.Component {
    componentDidMount() {
        document.addEventListener('click', () => {
            alert('document click');
        })
    }

    outClick(e) {
        console.log(e.currentTarget);
        alert('outClick');
    }
    
    onClick(e) {
        console.log(e.currentTarget);
        alert('onClick');
        e.stopPropagation();
    }
    render() {
        return <div onClick={this.outClick}>
            <button onClick={this.onClick}> 测试click事件 </button>
        </div>
    }
}

分析源码之前,有些工作和知识要提前准备,普及一下:

  1. 请各位准备好一个编辑器,自行用react-starter-kit建一个react项目,复制上面的代码,渲染上面的组件,然后打开控制台
  2. 下图是整个事件机制的流程图,后面会分部分解析
    https://www.processon.com/dia...
  3. 普及几个功能函数,提前了解它的作用
// 作用:如果只是单个next,则直接返回,如果有数组,返回合成的数组,里面有个
//current.push.apply(current, next)可以学习一下,我查了一下[资料][3]https://jsperf.com/array-prototype-push-apply-vs-concat/2,这样组合数组比concat效率更高

// 栗子:input accumulateInto([],[])

function accumulateInto(current, next) {
  if (current == null) {
    return next;
  }

  // Both are not empty. Warning: Never call x.concat(y) when you are not
  // certain that x is an Array (x could be a string with concat method).
  if (Array.isArray(current)) {
    if (Array.isArray(next)) {
      current.push.apply(current, next);
      return current;
    }
    current.push(next);
    return current;
  }

  if (Array.isArray(next)) {
    // A bit too dangerous to mutate `next`.
    return [current].concat(next);
  }

  return [current, next];
}

// 这个其实就是用来执行函数的,当arr时数组的时候,arr里的每一个项都作为回调函数cb的参数执行;
// 如果不是数组,直接执行回调函数cb,参数为arr
// 例如:
// arr为数组:forEachAccumulated([1,2,3], (item) => {console.log(item), this})
// 此时会打印出 1,2,3
// arr不为数组,forEachAccumulated(1, (item) => {console.log(item), this})
// 此时会打印出 1

function forEachAccumulated(arr, cb, scope) {
  if (Array.isArray(arr)) {
    arr.forEach(cb, scope);
  } else if (arr) {
    cb.call(scope, arr);
  }
}

React事件机制

React事件机制分为两块:

  • 事件注册
  • 事件分发

我们一步步来看:

事件注册

整个过程从ReactDomComponent开始,重点在enqueuePutListener,这个函数做了三件事情,详细请参考下面源码:

ReactDomComponent.js

function enqueuePutListener () {
    // 省略部分代码
    ...
    // 1、*重要:在这里取出button所在的document*
      var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
    // 2、在document上注册事件,同一个事件类型只会被注册一次
      listenTo(registrationName, doc);
    // 3、mountReady之后将回调函数存在ListernBank中
      transaction.getReactMountReady().enqueue(putListener, {
        inst: inst,
        registrationName: registrationName,
        listener: listener
      });
}

接下来看看第二步:在document上注册事件 的过程,流程图如下:

接着我们抽出每个文件的重点函数出来分析:

ReactBrowserEventEmitter.js

listenTo: function (registrationName, contentDocumentHandle) {
    var mountAt = contentDocumentHandle;
    // 检测document上是否已经监听onClick事件,所以前面说同一类型事件只会绑定一次
    var isListening = getListeningForDocument(mountAt);
    // 获得dependency,将onClick 转成topClick,这只是一种处理方式不用纠结
    var dependencies = 
   EventPluginRegistry.registrationNameDependencies[registrationName];
   // 中间是对各种事件类型给document绑定捕获事件或者冒泡事件,大部分都是冒泡,
   ...
   // 这里我们的topClick,绑定的是冒泡事件
   else if (topEventMapping.hasOwnProperty(dependency)) {  
   // trapBubbledEvent会在下面分析
 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
   }
   // 最后把topClick标记为已注册过,防止重复注册
   isListening[dependency] = true;
}

由于onclick绑定的是冒泡事件,所以我们来看看trapBubbledEvent

ReactEventListener.js

// 输入: topClick, click, doc
trapBubbledEvent: function (topLevelType, handlerBaseName, element) {
    if (!element) {
      return null;
    }
    //  EventListener 要做的事情就是把事件绑定到document上,注意这里无论是注册冒泡还是捕获事件,最终的回调函数都是dispatchEvent
    
    return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
  },
  
// EventListener.js
// 输入doc, click, dispatchEvent
// 这个函数其实就是我们熟悉的兼容浏IE浏览器事件绑定的方法
listen: function listen(target, eventType, callback) {
    if (target.addEventListener) {
      target.addEventListener(eventType, callback, false);
      return {
        remove: function remove() {
          target.removeEventListener(eventType, callback, false);
        }
      };
    } else if (target.attachEvent) {
      target.attachEvent('on' + eventType, callback);
      return {
        remove: function remove() {
          target.detachEvent('on' + eventType, callback);
        }
      };
    }
  },

注意这里无论是注册冒泡还是捕获事件,最终的回调函数都是dispatchEvent,所以我们来看看dispatchEvent怎么处理事件分发。

dispatchEvent

看到这里大家会奇怪,所有的事件的回调函数都是dispatchEvent来处理,那事件onClick原来的回调函数存到哪里去了呢?

再回来看事件注册的第三步:mountReady之后将回调函数存在ListernBank中

ReactDomComponent.js

function enqueuePutListener () {
    // 省略部分代码
    ...
    // 1、*重要:在这里取出button所在的document*
      var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
    // 2、在document上注册事件,同一个事件类型只会被注册一次
      listenTo(registrationName, doc);
    // 3、mountReady之后将回调函数存在ListernBank中
      transaction.getReactMountReady().enqueue(putListener, {
        inst: inst,
        registrationName: registrationName,
        listener: listener
      });
}

document上注册完所有的事件之后,还需要把listener 放到listenerBank中以listenerBank[registrationName][key] 这样的形式存起来,然后在dispatchEvent里面使用。

将listener放到listenerBank中储存的过程如下:

ReactDomComponent.js

// 在putListener里存入listener
function putListener() {
  var listenerToPut = this;
  // 先put的是外层的listener - outClick,所以这里的inst是外层div
  // registrationName是onclick,listener是outClick
  EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
}

EventPluginHub.js

  /**
   * Stores `listener` at `listenerBank[registrationName][key]`. Is idempotent.
   *
   * @param {object} inst The instance, which is the source of events.
   * @param {string} registrationName Name of listener (e.g. `onClick`).
   * @param {function} listener The callback to store.
   */
  putListener: function (inst, registrationName, listener) {
    var key = getDictionaryKey(inst); // 先根据inst得到唯一的key
    var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
    // 可以看到最终listener 在 listenerBank里,最终以listenerBank[registrationName][key] 存在

    bankForRegistrationName[key] = listener; 
   
    var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
    if (PluginModule && PluginModule.didPutListener) {
      // 这里的didPutListener只是为了兼容手机safari对non-interactive元素
      // 双击响应不正确,详情可以参考这篇[文章][7]
      //https://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
      PluginModule.didPutListener(inst, registrationName, listener);
    }
  },

以上就是事件注册的过程,接下来在看dispatchEvent如何处理事件分发。

事件分发

在介绍事件分发之前,有必要先介绍一下生成合成事件的过程,链接是https://segmentfault.com/a/11...

了解合成事件生成的过程之后,我们需要get一个点:合成事件收集了一波同类型(例如click)的回调函数存在了合成事件event._dispatchListeners这个数组里,然后将它们事件对应的虚拟dom节点放到_dispatchInstances 就本例来说,_dispatchListeners= [onClick, outClick],之后在一起执行。

接下来看看事件分发的过程:

EventListener.js

dispatchEvent: function (topLevelType, nativeEvent) {
    if (!ReactEventListener._enabled) {
      return;
    }
    // 这里得到TopLevelCallbackBookKeeping的实例对象,本例中第一次触发dispatchEvent时
    // bookKeeping = {ancestors: [],nativeEvent,‘topClick’}
    var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
    try {
      // Event queue being processed in the same cycle allows
      // `preventDefault`.
      // 接着执行handleTopLevelImpl(bookKeeping)
      ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
    } finally {
      TopLevelCallbackBookKeeping.release(bookKeeping);
    }
  }

function handleTopLevelImpl(bookKeeping) {
  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
  // 获取当前事件的虚拟dom元素
  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);

  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];
    // 这里的_handleTopLevel 对应的就是ReactEventEmitterMixin.js里的handleTopLevel
    ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
  }
}

// 这里的findParent曾经给我带来误导,我以为去找当前元素所有的父节点,但其实不是的,
// 我们知道一般情况下,我们的组件最后会被包裹在<div id='root'></div>的标签里
// 一般是没有组件再去嵌套它的,所以通常返回null
/**
 * Find the deepest React component completely containing the root of the
 * passed-in instance (for use when entire React trees are nested within each
 * other). If React trees are not nested, returns null.
 */
function findParent(inst) {
  while (inst._hostParent) {
    inst = inst._hostParent;
  }
  var rootNode = ReactDOMComponentTree.getNodeFromInstance(inst);
  var container = rootNode.parentNode;
  return ReactDOMComponentTree.getClosestInstanceFromNode(container);
}

上面这段代码的重点就是_handleTopLevel,它可以获取合成事件,并且去执行它。

下面看看具体是如何执行:

ReactEventEmitterMixin.js

function runEventQueueInBatch(events) {
  // 1、先将事件放进队列里
  EventPluginHub.enqueueEvents(events);
  // 2、执行它
  EventPluginHub.processEventQueue(false);
}

var ReactEventEmitterMixin = {
  /**
   * Streams a fired top-level event to `EventPluginHub` where plugins have the
   * opportunity to create `ReactEvent`s to be dispatched.
   */
  handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
     // 用EventPluginHub生成合成事件
    var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
    //  执行合成事件
    runEventQueueInBatch(events);
  }
};

执行的过程分成两步:

  1. 将事件放进队列
  2. 执行

执行的细节如下:

EventPluginHub.js

  var executeDispatchesAndReleaseTopLevel = function (e) {
    return executeDispatchesAndRelease(e, false);
  };

  var executeDispatchesAndRelease = function (event, simulated) {
      if (event) {
         // 在这里dispatch事件
        EventPluginUtils.executeDispatchesInOrder(event, simulated);
         // 释放事件
        if (!event.isPersistent()) {
          event.constructor.release(event);
        }
      }
  };

  enqueueEvents: function (events) {
    if (events) {
      eventQueue = accumulateInto(eventQueue, events);
    }
  },

  /**
   * Dispatches all synthetic events on the event queue.
   *
   * @internal
   */
  processEventQueue: function (simulated) {
    // Set `eventQueue` to null before processing it so that we can tell if more
    // events get enqueued while processing.
    var processingEventQueue = eventQueue;
    eventQueue = null;
    if (simulated) {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
    } else {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
    }
    // This would be a good time to rethrow if any of the event fexers threw.
    ReactErrorUtils.rethrowCaughtError();
  },

上段代码里,我们最终会走到

forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);

forEachAccumulated这个函数我们之前讲过,就是对数组processingEventQueue的每一个合成事件都使用executeDispatchesAndReleaseTopLevel来dispatch 事件。

所以各位同学们,注意到这里我们已经走到最核心的部分,dispatch 合成事件了,下面看看dispatch的详细过程:

EventPluginUtils.js

/**
 * Standard/simple iteration through an event's collected dispatches.
 */
function executeDispatchesInOrder(event, simulated) {
  var dispatchListeners = event._dispatchListeners;
  var dispatchInstances = event._dispatchInstances;

  if (Array.isArray(dispatchListeners)) {
    for (var i = 0; i < dispatchListeners.length; i++) {
     // 由这里可以看出,合成事件的stopPropagation只能阻止react合成事件的冒泡,
     // 因为event._dispatchListeners 只记录了由jsx绑定的绑定的事件,对于原生绑定的是没有记录的
      if (event.isPropagationStopped()) {
        break;
      }
      // Listeners and Instances are two parallel arrays that are always in sync.
      executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);
    }
  } else if (dispatchListeners) {
    executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

由上面的函数可知,dispatch 合成事件分为两个步骤:

  1. 通过_dispatchListeners里得到所有绑定的回调函数,在通过_dispatchInstances的绑定回调函数的虚拟dom元素
  2. 循环执行_dispatchListeners里所有的回调函数,这里有一个特殊情况,也是react阻止冒泡的原理

当回调函数里使用了stopPropagation会使得数组后面的回调函数不能执行,这样就做到了阻止事件冒泡

目前还是还有看到执行事件的代码,在接着看:

EventPluginHub.js

function executeDispatch(event, simulated, listener, inst) {
  var type = event.type || 'unknown-event';
  // 注意这里将事件对应的dom元素绑定到了currentTarget上
  event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
  if (simulated) {
    ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
  } else {
    // 一般都是非模拟的情况,执行invokeGuardedCallback
    ReactErrorUtils.invokeGuardedCallback(type, listener, event);
  }
  event.currentTarget = null;
}

上面这个函数最重要的功能就是将事件对应的dom元素绑定到了currentTarget上,
这样我们通过e.currentTarget就可以找到绑定事件的原生dom元素。

下面就是整个执行过程的尾声了:

ReactErrorUtils.js

var fakeNode = document.createElement('react');
ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {
  var boundFunc = function () {
    func(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);
};

invokeGuardedCallback可知,最后react调用了faked元素的dispatchEvent方法来触发事件,并且触发完毕之后立即移除监听事件。

总的来说,整个click事件被分发的过程就是:
1、用EventPluginHub生成合成事件,这里注意同一事件类型只会生成一个合成事件,里面的_dispatchListeners里储存了同一事件类型的所有回调函数

2、按顺序去执行它

就辣么简单!

本文比较长,有不理解的欢迎提问~ 或者有理解错误的也请大家指正。
最后附上整个流程图文件:

clipboard.png

s://segmentfault.com/a/1190000013343819

查看原文

幽_月 评论了文章 · 2019-01-17

一看就晕的React事件机制

前言

上篇文章我们了解了React合成事件跟原生绑定事件是有区别的,本篇文章从源码来深挖一下React的事件机制。

TL;DR :

  • react事件机制分为两个部分:1、事件注册 2、事件分发
  • 事件注册部分,所有的事件都会注册到document上,拥有统一的回调函数dispatchEvent来执行事件分发
  • 事件分发部分,首先生成合成事件,注意同一种事件类型只能生成一个合成事件Event,如onclick这个类型的事件,dom上所有带有通过jsx绑定的onClick的回调函数都会按顺序(冒泡或者捕获)会放到Event._dispatchListeners 这个数组里,后面依次执行它。

还是使用上次的栗子:

class ExampleApplication extends React.Component {
    componentDidMount() {
        document.addEventListener('click', () => {
            alert('document click');
        })
    }

    outClick(e) {
        console.log(e.currentTarget);
        alert('outClick');
    }
    
    onClick(e) {
        console.log(e.currentTarget);
        alert('onClick');
        e.stopPropagation();
    }
    render() {
        return <div onClick={this.outClick}>
            <button onClick={this.onClick}> 测试click事件 </button>
        </div>
    }
}

分析源码之前,有些工作和知识要提前准备,普及一下:

  1. 请各位准备好一个编辑器,自行用react-starter-kit建一个react项目,复制上面的代码,渲染上面的组件,然后打开控制台
  2. 下图是整个事件机制的流程图,后面会分部分解析
    https://www.processon.com/dia...
  3. 普及几个功能函数,提前了解它的作用
// 作用:如果只是单个next,则直接返回,如果有数组,返回合成的数组,里面有个
//current.push.apply(current, next)可以学习一下,我查了一下[资料][3]https://jsperf.com/array-prototype-push-apply-vs-concat/2,这样组合数组比concat效率更高

// 栗子:input accumulateInto([],[])

function accumulateInto(current, next) {
  if (current == null) {
    return next;
  }

  // Both are not empty. Warning: Never call x.concat(y) when you are not
  // certain that x is an Array (x could be a string with concat method).
  if (Array.isArray(current)) {
    if (Array.isArray(next)) {
      current.push.apply(current, next);
      return current;
    }
    current.push(next);
    return current;
  }

  if (Array.isArray(next)) {
    // A bit too dangerous to mutate `next`.
    return [current].concat(next);
  }

  return [current, next];
}

// 这个其实就是用来执行函数的,当arr时数组的时候,arr里的每一个项都作为回调函数cb的参数执行;
// 如果不是数组,直接执行回调函数cb,参数为arr
// 例如:
// arr为数组:forEachAccumulated([1,2,3], (item) => {console.log(item), this})
// 此时会打印出 1,2,3
// arr不为数组,forEachAccumulated(1, (item) => {console.log(item), this})
// 此时会打印出 1

function forEachAccumulated(arr, cb, scope) {
  if (Array.isArray(arr)) {
    arr.forEach(cb, scope);
  } else if (arr) {
    cb.call(scope, arr);
  }
}

React事件机制

React事件机制分为两块:

  • 事件注册
  • 事件分发

我们一步步来看:

事件注册

整个过程从ReactDomComponent开始,重点在enqueuePutListener,这个函数做了三件事情,详细请参考下面源码:

ReactDomComponent.js

function enqueuePutListener () {
    // 省略部分代码
    ...
    // 1、*重要:在这里取出button所在的document*
      var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
    // 2、在document上注册事件,同一个事件类型只会被注册一次
      listenTo(registrationName, doc);
    // 3、mountReady之后将回调函数存在ListernBank中
      transaction.getReactMountReady().enqueue(putListener, {
        inst: inst,
        registrationName: registrationName,
        listener: listener
      });
}

接下来看看第二步:在document上注册事件 的过程,流程图如下:

接着我们抽出每个文件的重点函数出来分析:

ReactBrowserEventEmitter.js

listenTo: function (registrationName, contentDocumentHandle) {
    var mountAt = contentDocumentHandle;
    // 检测document上是否已经监听onClick事件,所以前面说同一类型事件只会绑定一次
    var isListening = getListeningForDocument(mountAt);
    // 获得dependency,将onClick 转成topClick,这只是一种处理方式不用纠结
    var dependencies = 
   EventPluginRegistry.registrationNameDependencies[registrationName];
   // 中间是对各种事件类型给document绑定捕获事件或者冒泡事件,大部分都是冒泡,
   ...
   // 这里我们的topClick,绑定的是冒泡事件
   else if (topEventMapping.hasOwnProperty(dependency)) {  
   // trapBubbledEvent会在下面分析
 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
   }
   // 最后把topClick标记为已注册过,防止重复注册
   isListening[dependency] = true;
}

由于onclick绑定的是冒泡事件,所以我们来看看trapBubbledEvent

ReactEventListener.js

// 输入: topClick, click, doc
trapBubbledEvent: function (topLevelType, handlerBaseName, element) {
    if (!element) {
      return null;
    }
    //  EventListener 要做的事情就是把事件绑定到document上,注意这里无论是注册冒泡还是捕获事件,最终的回调函数都是dispatchEvent
    
    return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
  },
  
// EventListener.js
// 输入doc, click, dispatchEvent
// 这个函数其实就是我们熟悉的兼容浏IE浏览器事件绑定的方法
listen: function listen(target, eventType, callback) {
    if (target.addEventListener) {
      target.addEventListener(eventType, callback, false);
      return {
        remove: function remove() {
          target.removeEventListener(eventType, callback, false);
        }
      };
    } else if (target.attachEvent) {
      target.attachEvent('on' + eventType, callback);
      return {
        remove: function remove() {
          target.detachEvent('on' + eventType, callback);
        }
      };
    }
  },

注意这里无论是注册冒泡还是捕获事件,最终的回调函数都是dispatchEvent,所以我们来看看dispatchEvent怎么处理事件分发。

dispatchEvent

看到这里大家会奇怪,所有的事件的回调函数都是dispatchEvent来处理,那事件onClick原来的回调函数存到哪里去了呢?

再回来看事件注册的第三步:mountReady之后将回调函数存在ListernBank中

ReactDomComponent.js

function enqueuePutListener () {
    // 省略部分代码
    ...
    // 1、*重要:在这里取出button所在的document*
      var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
    // 2、在document上注册事件,同一个事件类型只会被注册一次
      listenTo(registrationName, doc);
    // 3、mountReady之后将回调函数存在ListernBank中
      transaction.getReactMountReady().enqueue(putListener, {
        inst: inst,
        registrationName: registrationName,
        listener: listener
      });
}

document上注册完所有的事件之后,还需要把listener 放到listenerBank中以listenerBank[registrationName][key] 这样的形式存起来,然后在dispatchEvent里面使用。

将listener放到listenerBank中储存的过程如下:

ReactDomComponent.js

// 在putListener里存入listener
function putListener() {
  var listenerToPut = this;
  // 先put的是外层的listener - outClick,所以这里的inst是外层div
  // registrationName是onclick,listener是outClick
  EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
}

EventPluginHub.js

  /**
   * Stores `listener` at `listenerBank[registrationName][key]`. Is idempotent.
   *
   * @param {object} inst The instance, which is the source of events.
   * @param {string} registrationName Name of listener (e.g. `onClick`).
   * @param {function} listener The callback to store.
   */
  putListener: function (inst, registrationName, listener) {
    var key = getDictionaryKey(inst); // 先根据inst得到唯一的key
    var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
    // 可以看到最终listener 在 listenerBank里,最终以listenerBank[registrationName][key] 存在

    bankForRegistrationName[key] = listener; 
   
    var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
    if (PluginModule && PluginModule.didPutListener) {
      // 这里的didPutListener只是为了兼容手机safari对non-interactive元素
      // 双击响应不正确,详情可以参考这篇[文章][7]
      //https://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
      PluginModule.didPutListener(inst, registrationName, listener);
    }
  },

以上就是事件注册的过程,接下来在看dispatchEvent如何处理事件分发。

事件分发

在介绍事件分发之前,有必要先介绍一下生成合成事件的过程,链接是https://segmentfault.com/a/11...

了解合成事件生成的过程之后,我们需要get一个点:合成事件收集了一波同类型(例如click)的回调函数存在了合成事件event._dispatchListeners这个数组里,然后将它们事件对应的虚拟dom节点放到_dispatchInstances 就本例来说,_dispatchListeners= [onClick, outClick],之后在一起执行。

接下来看看事件分发的过程:

EventListener.js

dispatchEvent: function (topLevelType, nativeEvent) {
    if (!ReactEventListener._enabled) {
      return;
    }
    // 这里得到TopLevelCallbackBookKeeping的实例对象,本例中第一次触发dispatchEvent时
    // bookKeeping = {ancestors: [],nativeEvent,‘topClick’}
    var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
    try {
      // Event queue being processed in the same cycle allows
      // `preventDefault`.
      // 接着执行handleTopLevelImpl(bookKeeping)
      ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
    } finally {
      TopLevelCallbackBookKeeping.release(bookKeeping);
    }
  }

function handleTopLevelImpl(bookKeeping) {
  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
  // 获取当前事件的虚拟dom元素
  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);

  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];
    // 这里的_handleTopLevel 对应的就是ReactEventEmitterMixin.js里的handleTopLevel
    ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
  }
}

// 这里的findParent曾经给我带来误导,我以为去找当前元素所有的父节点,但其实不是的,
// 我们知道一般情况下,我们的组件最后会被包裹在<div id='root'></div>的标签里
// 一般是没有组件再去嵌套它的,所以通常返回null
/**
 * Find the deepest React component completely containing the root of the
 * passed-in instance (for use when entire React trees are nested within each
 * other). If React trees are not nested, returns null.
 */
function findParent(inst) {
  while (inst._hostParent) {
    inst = inst._hostParent;
  }
  var rootNode = ReactDOMComponentTree.getNodeFromInstance(inst);
  var container = rootNode.parentNode;
  return ReactDOMComponentTree.getClosestInstanceFromNode(container);
}

上面这段代码的重点就是_handleTopLevel,它可以获取合成事件,并且去执行它。

下面看看具体是如何执行:

ReactEventEmitterMixin.js

function runEventQueueInBatch(events) {
  // 1、先将事件放进队列里
  EventPluginHub.enqueueEvents(events);
  // 2、执行它
  EventPluginHub.processEventQueue(false);
}

var ReactEventEmitterMixin = {
  /**
   * Streams a fired top-level event to `EventPluginHub` where plugins have the
   * opportunity to create `ReactEvent`s to be dispatched.
   */
  handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
     // 用EventPluginHub生成合成事件
    var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
    //  执行合成事件
    runEventQueueInBatch(events);
  }
};

执行的过程分成两步:

  1. 将事件放进队列
  2. 执行

执行的细节如下:

EventPluginHub.js

  var executeDispatchesAndReleaseTopLevel = function (e) {
    return executeDispatchesAndRelease(e, false);
  };

  var executeDispatchesAndRelease = function (event, simulated) {
      if (event) {
         // 在这里dispatch事件
        EventPluginUtils.executeDispatchesInOrder(event, simulated);
         // 释放事件
        if (!event.isPersistent()) {
          event.constructor.release(event);
        }
      }
  };

  enqueueEvents: function (events) {
    if (events) {
      eventQueue = accumulateInto(eventQueue, events);
    }
  },

  /**
   * Dispatches all synthetic events on the event queue.
   *
   * @internal
   */
  processEventQueue: function (simulated) {
    // Set `eventQueue` to null before processing it so that we can tell if more
    // events get enqueued while processing.
    var processingEventQueue = eventQueue;
    eventQueue = null;
    if (simulated) {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
    } else {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
    }
    // This would be a good time to rethrow if any of the event fexers threw.
    ReactErrorUtils.rethrowCaughtError();
  },

上段代码里,我们最终会走到

forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);

forEachAccumulated这个函数我们之前讲过,就是对数组processingEventQueue的每一个合成事件都使用executeDispatchesAndReleaseTopLevel来dispatch 事件。

所以各位同学们,注意到这里我们已经走到最核心的部分,dispatch 合成事件了,下面看看dispatch的详细过程:

EventPluginUtils.js

/**
 * Standard/simple iteration through an event's collected dispatches.
 */
function executeDispatchesInOrder(event, simulated) {
  var dispatchListeners = event._dispatchListeners;
  var dispatchInstances = event._dispatchInstances;

  if (Array.isArray(dispatchListeners)) {
    for (var i = 0; i < dispatchListeners.length; i++) {
     // 由这里可以看出,合成事件的stopPropagation只能阻止react合成事件的冒泡,
     // 因为event._dispatchListeners 只记录了由jsx绑定的绑定的事件,对于原生绑定的是没有记录的
      if (event.isPropagationStopped()) {
        break;
      }
      // Listeners and Instances are two parallel arrays that are always in sync.
      executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);
    }
  } else if (dispatchListeners) {
    executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

由上面的函数可知,dispatch 合成事件分为两个步骤:

  1. 通过_dispatchListeners里得到所有绑定的回调函数,在通过_dispatchInstances的绑定回调函数的虚拟dom元素
  2. 循环执行_dispatchListeners里所有的回调函数,这里有一个特殊情况,也是react阻止冒泡的原理

当回调函数里使用了stopPropagation会使得数组后面的回调函数不能执行,这样就做到了阻止事件冒泡

目前还是还有看到执行事件的代码,在接着看:

EventPluginHub.js

function executeDispatch(event, simulated, listener, inst) {
  var type = event.type || 'unknown-event';
  // 注意这里将事件对应的dom元素绑定到了currentTarget上
  event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
  if (simulated) {
    ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
  } else {
    // 一般都是非模拟的情况,执行invokeGuardedCallback
    ReactErrorUtils.invokeGuardedCallback(type, listener, event);
  }
  event.currentTarget = null;
}

上面这个函数最重要的功能就是将事件对应的dom元素绑定到了currentTarget上,
这样我们通过e.currentTarget就可以找到绑定事件的原生dom元素。

下面就是整个执行过程的尾声了:

ReactErrorUtils.js

var fakeNode = document.createElement('react');
ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {
  var boundFunc = function () {
    func(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);
};

invokeGuardedCallback可知,最后react调用了faked元素的dispatchEvent方法来触发事件,并且触发完毕之后立即移除监听事件。

总的来说,整个click事件被分发的过程就是:
1、用EventPluginHub生成合成事件,这里注意同一事件类型只会生成一个合成事件,里面的_dispatchListeners里储存了同一事件类型的所有回调函数

2、按顺序去执行它

就辣么简单!

本文比较长,有不理解的欢迎提问~ 或者有理解错误的也请大家指正。
最后附上整个流程图文件:

clipboard.png

s://segmentfault.com/a/1190000013343819

查看原文

幽_月 赞了文章 · 2018-12-29

一篇文章理解Web缓存

最近把前端缓存重新整理了一下,从整体的层面上把前端所有能用的缓存方案梳理了一遍。同时,对于http缓存,使用了表格的方案,使得原先晦涩难记的特性变得清晰明了。特记录于此,若有什么欠缺,也望不吝指出。

1. 前端缓存概述

前端缓存主要是分为HTTP缓存和浏览器缓存。其中HTTP缓存是在HTTP请求传输时用到的缓存,主要在服务器代码上设置;而浏览器缓存则主要由前端开发在前端js上进行设置。下面会分别具体描述。

clipboard.png

2. 前端缓存分类

2.1 HTTP缓存

整体流程:HTTP缓存都是从第二次请求开始的。
第一次请求资源时,服务器返回资源,并在respone header头中回传资源的缓存参数;第二次请求时,浏览器判断这些请求参数,击中强缓存就直接200,否则就把请求参数加到request header头中传给服务器,看是否击中协商缓存,击中则返回304,否则服务器会返回新的资源。

HTTP缓存分为强缓存和协议缓存,它们的区别如下:

clipboard.png

200 from disk or 200 from memory :
强缓存的200也有两种情况:200 from disk和200 from memory。现在我没有找到明确的文档来描述这种区别的发生条件。知乎这个问题中提到了一些情景,可以自行取用。

2.1.1 强缓存

clipboard.png

2.1.2 协商缓存

协商缓存都是成对出现的。
clipboard.png

2.1.3 最佳优化策略-消灭304

最佳优化策略:因为协商缓存本身也有http请求的损耗,所以最佳优化策略是要尽可能的将静态文件存储为较长的时间,多利用强缓存而不是协商缓存,即消灭304。

但是给文件设置一个很长的Cacha-Control也会带来其他的问题,最主要的问题是静态内容更新时,用户不能及时获得更新的内容。这时候就要使用hash的方法对文件进行命名,通过每次更新不同的静态文件名来消除强缓存的影响。

Hash命名:
http://xxx.com/main.5eas34fa.js
http://xxx.com/main.js?5eas34fa
http://xxx.com/5eas34fa/main.js

2.2 浏览器缓存

2.2.1 本地存储小容量

Cookie主要用于用户信息的存储,Cookie的内容可以自动在请求的时候被传递给服务器。
LocalStorage的数据将一直保存在浏览器内,直到用户清除浏览器缓存数据为止。
SessionStorage的其他属性同LocalStorage,只不过它的生命周期同标签页的生命周期,当标签页被关闭时,SessionStorage也会被清除。

clipboard.png

2.2.2 本地存储大容量

WebSql和IndexDB主要用在前端有大容量存储需求的页面上,例如,在线编辑浏览器或者网页邮箱。

clipboard.png

2.2.3 应用缓存与PWA

应用缓存全称为Offline Web Application,它的缓存内容被存在浏览器的Application Cache中。它也是一个被W3C标准废弃的功能,主要是通过manifest文件来标注要被缓存的静态文件清单。但是在缓存静态文件的同时,也会默认缓存html文件。这导致页面的更新只能通过manifest文件中的版本号来决定。而且,即使我们更新了version,用户的第一次访问还是会访问到老的页面,只有下一次再访问才能访问到新的页面。所以,应用缓存只适合那种常年不变化的静态网站。如此的不方便,也是被废弃的重要原因。

PWA全称是渐进式网络应用,主要目标是实现web网站的APP式功能和展示。尽管PWA也有manifest文件,但是与应用缓存却完全不同。不同于manifest简单的将文件通过是否缓存进行分类,PWA用manifest构建了自己的APP骨架。另外,PWA用Service Worker来控制缓存的使用。这一块的内容较多,在这里就不详细展开了。

clipboard.png

2.2.4 往返缓存

往返缓存又称为BFCache,是浏览器在前进后退按钮上为了提升历史页面的渲染速度的一种策略。BFCache会缓存所有的DOM结构,但是问题在于,一些页面开始时进行的上报或者请求可能会被影响。这个问题现在主要会出现在微信h5的开发中

去除BFCache有多种方法,但不是本文的重点,想了解的同学可以看《浏览器往返缓存(Back/Forward cache)问题的分析与解决

总结

本文梳理了前端所有可能涉及的缓存,希望能从整体层面建立起系统的缓存知识体系。限于篇幅,每一部分的描述都比较简略,仅起到抛砖引玉之用。如有错误,还望指出。

查看原文

赞 113 收藏 93 评论 5

幽_月 赞了回答 · 2018-11-04

解决defer和async的区别

说那么多干嘛,一图胜千言!

wfL82.png

关注 69 回答 6

幽_月 收藏了文章 · 2018-10-18

html-webpack-plugin用法全解

本文只在个人博客和 SegmentFault 社区个人专栏发表,转载请注明出处
个人博客: https://zengxiaotao.github.io
SegmentFault 个人专栏: https://segmentfault.com/blog...

html-webpack-plugin 可能用过的 webpack 的童鞋都用过这个 plugin ,就算没用过可能也听过。我们在学习webpack的时候,可能经常会看到这样的一段代码。

// webpack.config.js
module.exports = {
    entry: path.resolve(__dirname, './app/index.js'),
    output:{
        path: path.resolve(__dirname, './build'),
        filename: 'bundle.js'
    }
    ...
    plugins: [
        new HtmlWebpackPlugin()
    ]
}

之后在终端输入 webpack 命令后

webpack

你会神奇的看到在你的 build 文件夹会生成一个 index.html 文件和一个 bundle.js 文件,而且 index.html 文件中自动引用 webpack 生成的 bundle.js 文件。

所有的这些都是 html-webpack-plugin 的功劳。它会自动帮你生成一个 html 文件,并且引用相关的 assets 文件(如 css, js)。

自己在六月第一次接触前端自动化构建,学习 webpack 和 react 时,曾经简单使用过这个插件,但也只是用了常见的几个选项,今天就跟着官方文档走一走,看看它的所有用法。

title

顾名思义,设置生成的 html 文件的标题。

filename

也没什么说的,生成 html 文件的文件名。默认为 index.html.

template

根据自己的指定的模板文件来生成特定的 html 文件。这里的模板类型可以是任意你喜欢的模板,可以是 html, jade, ejs, hbs, 等等,但是要注意的是,使用自定义的模板文件时,需要提前安装对应的 loader, 否则webpack不能正确解析。以 jade 为例。

npm install jade-loader --save-dev
// webpack.config.js
...
loaders: {
    ...
    {
        test: /\.jade$/,
        loader: 'jade'
    }
}
plugins: [
    new HtmlWebpackPlugin({
        ...
        jade: 'path/to/yourfile.jade'
    })
]

最终在build文件夹内会生成一个 yourfile.html 和 bundle.js 文件。现在我们再回头来看看之前将的 title 属性。

如果你既指定了 template 选项,又指定了 title 选项,那么webpack 会选择哪一个? 事实上,这时候会选择你指定的模板文件的 title, 即使你的模板文件中未设置 title

那么 filename 呢,是否也会覆盖,其实不是,会以指定的 filename 作为文件名。

inject

注入选项。有四个选项值 true, body, head, false.

  • true

    • 默认值,script标签位于html文件的 body 底部

  • body

    • 同 true

  • head

    • script 标签位于 head 标签内

  • false

    • 不插入生成的 js 文件,只是单纯的生成一个 html 文件

favicon

给生成的 html 文件生成一个 favicon。属性值为 favicon 文件所在的路径名。

// webpack.config.js
...
plugins: [
    new HtmlWebpackPlugin({
        ...
        favicon: 'path/to/yourfile.ico'
    }) 
]

生成的 html 标签中会包含这样一个 link 标签

<link rel="shortcut icon" href="example.ico">

同 title 和 filename 一样,如果在模板文件指定了 favicon,会忽略该属性。

minify

minify 的作用是对 html 文件进行压缩,minify 的属性值是一个压缩选项或者 false 。默认值为false, 不对生成的 html 文件进行压缩。来看看这个压缩选项。

html-webpack-plugin 内部集成了 html-minifier ,这个压缩选项同 html-minify 的压缩选项完全一样,
看一个简单的例子。

// webpack.config.js
...
plugins: [
    new HtmlWebpackPlugin({
        ...
        minify: {
            removeAttributeQuotes: true // 移除属性的引号
        }
    })
]
<!-- 原html片段 -->
<div id="example" class="example">test minify</div>
<!-- 生成的html片段 -->
<div id=example class=example>test minify</div>

hash

hash选项的作用是 给生成的 js 文件一个独特的 hash 值,该 hash 值是该次 webpack 编译的 hash 值。默认值为 false 。同样看一个例子。

// webpack.config.js
plugins: [
    new HtmlWebpackPlugin({
        ...
        hash: true
    })
]
<script type=text/javascript data-original=bundle.js?22b9692e22e7be37b57e></script>

执行 webpack 命令后,你会看到你的生成的 html 文件的 script 标签内引用的 js 文件,是不是有点变化了。
bundle.js 文件后跟的一串 hash 值就是此次 webpack 编译对应的 hash 值。

$ webpack
Hash: 22b9692e22e7be37b57e
Version: webpack 1.13.2

cache

默认值是 true。表示只有在内容变化时才生成一个新的文件。

showErrors

showErrors 的作用是,如果 webpack 编译出现错误,webpack会将错误信息包裹在一个 pre 标签内,属性的默认值为 true ,也就是显示错误信息。

chunks

chunks 选项的作用主要是针对多入口(entry)文件。当你有多个入口文件的时候,对应就会生成多个编译后的 js 文件。那么 chunks 选项就可以决定是否都使用这些生成的 js 文件。

chunks 默认会在生成的 html 文件中引用所有的 js 文件,当然你也可以指定引入哪些特定的文件。

看一个小例子。

// webpack.config.js
entry: {
    index: path.resolve(__dirname, './src/index.js'),
    index1: path.resolve(__dirname, './src/index1.js'),
    index2: path.resolve(__dirname, './src/index2.js')
}
...
plugins: [
    new HtmlWebpackPlugin({
        ...
        chunks: ['index','index2']
    })
]

执行 webpack 命令之后,你会看到生成的 index.html 文件中,只引用了 index.js 和 index2.js

...
<script type=text/javascript data-original=index.js></script>
<script type=text/javascript data-original=index2.js></script>

而如果没有指定 chunks 选项,默认会全部引用。

excludeChunks

弄懂了 chunks 之后,excludeChunks 选项也就好理解了,跟 chunks 是相反的,排除掉某些 js 文件。 比如上面的例子,其实等价于下面这一行

...
excludeChunks: ['index1.js']

chunksSortMode

这个选项决定了 script 标签的引用顺序。默认有四个选项,'none', 'auto', 'dependency', '{function}'。

  • 'dependency' 不用说,按照不同文件的依赖关系来排序。

  • 'auto' 默认值,插件的内置的排序方式,具体顺序这里我也不太清楚...

  • 'none' 无序? 不太清楚...

  • {function} 提供一个函数?但是函数的参数又是什么? 不太清楚...

如果有使用过这个选项或者知道其具体含义的同学,还请告知一下。。。

xhtml

一个布尔值,默认值是 false ,如果为 true ,则以兼容 xhtml 的模式引用文件。

总结

以上,就总结完了传入 new HtmlWebpackPlugin() 的选项,了解全部选项的含义后,可以在项目构建时更灵活的使用。

全文完

查看原文

幽_月 赞了文章 · 2018-10-10

2018前端面试准备

前端面试常见问题按知识点分类整理

前端面试常考问题整理,按模块知识点分类,持续完善中... Front-end-Developer-Questions by Modules and knowledge

前端面试经典问题:CSS 中居中的几种方式

面试中经常遇到的面试题之一,居中布局,特来总结

几个让我印象深刻的面试题 (一)

分享几个我遇到的认为不错的面试题

27款优质简洁的个人简历打包下载

优质简洁的个人简历打包

前端开发面试题总结之——JAVASCRIPT(一)

前端面试系列之 JavaScript,还有两篇

44 个 JavaScript 变态题解析

读者可以先去做一下感受感受. 当初笔者的成绩是 21/44...

当初笔者做这套题的时候不仅怀疑智商, 连人生都开始怀疑了....

不过, 对于基础知识的理解是深入编程的前提. 让我们一起来看看这些变态题到底变态不变态吧!

技术 | 前端面试题(二):自定义事件

我和阿里巴巴的同事守雌将为大家带来一个系列专题:前端面试题解析,一周更新两篇,本篇主要讲如何实现自定义事件。

前端开发面试题总结之——JAVASCRIPT(二)

前端开发面试题总结之—JavaScript,一共三篇

前端开发面试题总结之——HTML

前端开发面试题,本文主要讲述的是 HTML 相关的面试题。面试相关的我整理成收藏集欢迎关注。

收集 JavaScript 各种疑难杂症的问题集锦

关于 JavaScript,工作和学习过程中遇到过许多问题,也解答过许多别人的问题。这篇文章记录了一些有价值的问题。

一道 JS 面试题所引发的 "血案",透过现象寻本质,再从本质看现象

由一道面试题想到拓展的,分析问题的方法

CSS 面试题解答

什么是 CSS reset?CSS 性能优化?浮动的原理和工作方式,会产生什么影响呢,要怎么处理?CSS 权重?

Excuse me?这个前端面试在搞事!

金三银四搞事季,前端这个近年的热门领域,搞事气氛特别强烈,我朋友小伟最近就在疯狂面试,遇到了许多有趣的面试官,有趣的面试题,我来帮这个搞事 boy 转述一下。

前端开发面试题总结之——JAVASCRIPT(三)

前端开发面试题总结之 JavaScript,一共三篇

1月前端面试记

背景 我于16.12.18辞职,之前有过一年左右的前端工作经验。从12月26号开始到1月9号先后面试了微信,百度,阿里巴巴uc,唯品会以及深圳腾讯等几家公司,特此总结与各位共勉。 微信 由于我已经毕业工作过,所以去微信面试是走的社招。微信社招极其严格,共八轮面试,总体来说我基本…

一道面试题引发的对 javascript 类型转换的思考

通过一道面试题,详细描述了 javascript 类型转换的相关内容,对 javascript 进阶有所帮助。

献给前端求职路上的你们(上)

我是一名前端开发,从2016年6月毕业到如今步入工作,期间也面试了一些公司,参考过一些面试文档,学习了一些面试宝典,掌握了一些面试、笔试技巧和经验,所以就总结了一些优质的前端面试题以及面试要点,初学者阅后也要用心钻研其中的原理,重要知识需要系统学习,透彻学习,才能形成自己的知识链,以不变应万变,万不可投机取巧,只求面试过关哦!

征服 JavaScript 面试:什么是闭包

征服 JavaScript 面试:什么是闭包

一个普通本科在校生的前端学习之路

今天我分享的内容主要是关于前端初学者的学习路线和一些建议,还有自己在准备校招过程中的一点经验。

CSS 并不简单 -- 一道微信面试题的实践

本系列会持续分享本人学习到的 CSS 知识点、技巧和效果展示。如有错误,希望您能指出。

从培训班出来之后找工作的经历,教会了我这五件事

这是 Medium 上的一篇文章(已有 5900 个赞),讲的是国外一个培训出来的程序员,用三个月时间,找到了一份年薪 12 万美元的工作,并从中得到的五个忠告的故事。 我觉得他总结得很好,尤其是心态和方法,非常值得学习。对正在找工作的同学非常有用。 想收到更多最新、最专业的前…

最近遇到的前端面试题 (2017.02.23 更新版)

最近整理的前端面试题,希望能对大家有帮助。转载自:http://www.jianshu.com/p/3944...

17 年 2 月面试经验 | _striveg blog

个人面试几家的面试题和一点小感悟

面试 -- 网络 HTTP

现在面试门槛越来越高,很多开发者对于网络知识这块了解的不是很多,遇到这些面试题会手足无措。本篇文章知识主要集中在 HTTP 这块。文中知识来自 《图解 HTTP》与维基百科,若有错误请大家指出。文章会持续更新。 面试 -- 网络 TCP/IP 了解 Web 及网络基础 对端传输…

从一道面试题,到 “我可能看了假源码”

今天想谈谈一道前端面试题

技术 | 前端面试题(一):递归解析

我和阿里巴巴的同事守雌将为大家带来一个系列专题:前端面试题解析,一周更新两篇,也许答案可能不是最优的,但是也可以给你提供解决问题的思路。

谈谈面试与面试题

Winter 对于前端面试的一些看法。

查看原文

赞 560 收藏 814 评论 6

认证与成就

  • 获得 99 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-09-02
个人主页被 1.6k 人浏览