Code Monkey home page Code Monkey logo

frontend's Introduction

😄我叫andy,因为喜欢刘德华,所以取了这个英文名称

29岁转行做前端,一路都非常辛苦,也非常开心,能找到自己喜欢做的事情,我觉得自己是幸运的!

andy's GitHub stats

frontend's People

Contributors

andychenan avatar dependabot[bot] avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

yubei826

frontend's Issues

react组件

react组件

组件从概念上看就像是一个函数,它可以接受任意的输入值(称之为"props"),并返回一个需要在页面上展示的React元素。(来自React官网的定义

我们都知道react.js是一个用户构建用户界面的JavaScript库。而构建用户界面的最基本单元就是react元素。一个页面可以分为很多部分,而每一个部分都是由一个一个的组件所构成,所以说,react.js的其中一个特点就是组件化开发。

如何创建一个react组件

函数定义组件

定义一个组件最简单的方式就是使用JavaScript函数,函数定义的组件是无状态组件

const App = (props) => {
	return <div>hello andy</div>;
}
类定义组件

类定义的组件是有状态组件

class App extends Component {
	constructor (props) {
		super(props);
	}
	render () {
		return <div>hello andy</div>;
	}
}

注意:无论是函数定义的组件还是类定义的组件,都不能修改自己的props。

class App extends Component {
	constructor (props) {
		super(props);
	}
	render () {
		return <Button name="andy" />
	}
}

class Button extends Component {
	constructor (props) {
		super(props);
	}
	clickHandler () {
		// 这里是不能修改props的值的,如果修改的话会直接报错
		this.props.name = 'jack';
	}
	render () {
		return <button onClick={() => this.clickHandler()}>click now!</button>
	}
}

上面代码当点击按钮时,会报错:该属性不能修改。

TypeError: Cannot assign to read only property 'name' of object '#<Object>'

有状态组件和无状态组件

有状态组件指的是,组件中存在state,而无状态组件指的是,组件中不存在state,它只是一个简单的组件,接受props作为参数,来展示内容。

无状态组件:

class List extends Component {
	constructor (props) {
		super(props);
	}
	render () {
		const nameList = this.props.names.map(name => (
			<li key={name}>{name}</li>
		));
		return (
			<ul>
				{nameList}
			</ul>
		)
	}
}

class App extends Component {
	constructor (props) {
		super(props);
	}
	render () {
		const names = ['andy' , 'jack' , 'alex'];
		return <List names={names} />
	}
}

有状态组件:

class App extends Component {
	constructor (props) {
		super(props);
		this.state = {
			tick : 0
		}
	}
	onClickHandle () {
		this.setState({
			tick : ++this.state.tick
		})
	}
	render () {
		return (
			<div>
				<button onClick={() => this.onClickHandle()}>点我</button>
				<div>{this.state.tick}</div>
			</div>
		)
	}
}

展示组件和容器组件

如果我们将组件分为两类,会发现组件更加容易复用,而这两类组件可以称为展示组件和容器组件。

展示组件:
展示组件只关注页面展示,通常组件内部有一些DOM标签和组件自己的样式。如果不需要state,生命周期钩子,或者性能优化,那么一般都是写成函数式的组件。

容器组件:
容器组件主要是用来给展示组件提供数据,这样我们可以在容器组件中去获取数据,然后将数据通过props的形式传递给展示组件。

import React, { Component } from 'react';
import axios from 'axios';

// 这个容器组件主要负责获取数据,并通过props传递给子组件
class UserContainer extends Component {
	constructor (props) {
		super(props);
		this.state = {
			users : []
		}
	}
	componentDidMount () {
		axios.get("https://api.github.com/search/repositories?q=language:javascript&sort=stars")
		.then(res => {
			this.setState({
				users : res.data.items
			})
		})
		.catch(err => {
			console.log(err);
		})
	}
	render () {
		return <UserList users={this.state.users} />
	}
}

// 子组件主要负责页面展示
class UserList extends Component {
	constructor (props) {
		super(props);
	}
	render () {
		const { users } = this.props;
		const userList = users.map(user => (
			<li key={user.id}>{user.name}</li>
		));
		return (
			<ul>
				{userList}
			</ul>
		)
	}
}

class App extends Component {
	constructor (props) {
		super(props);
		this.state = {
			tick : 0
		}
	}
	render () {
		return (
			<UserContainer />
		)
	}
}

高阶组件

高阶组件就是一个函数,接受一个组件作为参数,返回一个新的组件。(这里就是用一个组件包装另一个组件)

高阶组件的实现方式

1、属性代理(props proxy)

高阶组件来操控传递给包装组件的属性。

2、继承反转

高阶组件继承包装组件。

props proxy

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      return <WrappedComponent {...this.props}/>
    }
  }
}

上面代码就是一个通过props proxy方式来实现高阶组件,这里最重要的部分就是HOC返回了一个WrappedComponent类型的React元素,并且也接收了props,所以这才叫做props proxy。

用函数作为子组件

组件接收一个函数作为他的子元素。比如:

class App extends Component {
    constructor (props) {
        super(props);
    }
    render () {
        return (
            {this.props.children("andy")}
        )
    }
}

其实将子组件作为函数的用法比较简单,像下面这个例子:

const root = document.getElementById('root');

class Welcome extends React.Component {
    constructor (props) {
        super(props);
    }
    render () {
        return (
            <div>
                {this.props.children("welcome to China")}    
            </div>
        )
    }
}
class App extends React.Component {
    constructor (props) {
        super(props);
    }
    render () {
        return (
            <Welcome>
                {name => <h1>{name}</h1>}
            </Welcome>
        )
    }
}

ReactDOM.render(<App /> , root)
const root = document.getElementById('root');

class Welcome extends React.Component {
    constructor (props) {
        super(props);
    }
    render () {
        return (
            <div>
                {this.props.children('red' , 18 , 'andychen')}    
            </div>
        )
    }
}
class App extends React.Component {
    constructor (props) {
        super(props);
    }
    render () {
        return (
            <Welcome>
                {(color , fontSize , name) => <h1 style={{color , fontSize}}>{name}</h1>}
            </Welcome>
        )
    }
}

ReactDOM.render(<App /> , root)

上面的例子,我们可以看出通过函数作为子类组件的组件我们就能解耦父类组件和它们的子类组件,让我们决定使用哪些参数以及怎么将参数运用于子类组件中。

react的diff算法

react的diff算法

这篇文章很详细,介绍diff算法

注意:两棵树:workInProgress和workInProgress.alternate

tree diff

两棵树只会对同一层次的节点进行比较。

component diff

  • 如果是同类型组件,那么就按照原来的策略一层一层的比较虚拟DOM树
  • 如果是不同类型组件,那么就会创建一个新的组件来替换之前的组件(包括子元素)
function updateElement(returnFiber, current$$1, element, expirationTime) {
    // 如果两个组件的类型相同,那么只需要更新props就可以了
    // 如果两个组件的类型不同,那么就需要重新创建
    if (current$$1 !== null && current$$1.elementType === element.type) {
        // Move based on index
        var existing = useFiber(current$$1, element.props, expirationTime);
        existing.ref = coerceRef(returnFiber, current$$1, element);
        existing.return = returnFiber;
        {
            existing._debugSource = element._source;
            existing._debugOwner = element._owner;
        }
      return existing;
    } else {
        // Insert
        var created = createFiberFromElement(element, returnFiber.mode, expirationTime);
        created.ref = coerceRef(returnFiber, current$$1, element);
        created.return = returnFiber;
        return created;
    }
}

我们可以看一个例子:

class Andy extends Component {
    constructor (props) {
        super(props);
        console.log('andy constructor');
    }
    componentWillMount () {
        console.log('andy will mount');
    }
    componentWillUnmount () {
        console.log('andy will unmount');
    }
    componentDidMount () {
        console.log('andy did mount');
    }
    render () {
        console.log('andy render');
        return (
            <div>
                <div>hello andy</div>
                <div>good morning</div>
            </div>
        )
    }
};

class Jack extends Component {
    constructor (props) {
        super(props);
    }
    componentWillMount () {
        console.log('jack will mount');
    }
    componentDidMount () {
        console.log('jack did mount');
    }
    render () {
        console.log('jack render');
        return (
            <div>
                <div>hello jack</div>
                <div>good night</div>
            </div>
        )
    }
}

class App extends Component {
    constructor (props) {
        super(props);
        this.state = {
            show : true
        }
    }
    delete = (evt) => {
        this.setState({
            show : false
        })
    }
    render () {
        return (
            <div>
                {
                    this.state.show ? <Andy /> : <Jack />
                }
                <button onClick={this.delete}>delete</button>
            </div>
        )
    }
}

export default App;

当我点击delete按钮是,我们打印一下Andy组件和Jack组件的执行结果:

image

从打印结果,我们可以看出,如果是两个不同类型的组件,那么会删除掉之前的组件,然后重新挂载新的组件。

element diff

当节点处于同一层级时,react diff提供了三种节点操作,插入,移动,删除,如果只是移动位置,react会怎么做呢?,我们来举个例子:

class App extends Component {
    constructor (props) {
        super(props);
        this.state = {
            list : ['andy' , 'jack' , 'henry']
        }
    }
    change = () => {
        this.setState({
            list : ['jack' , 'andy' , 'henry']
        })
    }
    render () {
        return (
            <div>
                <div>
                    {
                        this.state.list.map(item => (
                            <p key={item}>{item}</p>
                        ))
                    }
                </div>
                <button onClick={this.change}>change</button>
            </div>
        )
    }
}

export default App;
  • 首先对新集合的节点进行循环遍历,通过唯一的key可以判断新老集合中是否存在相同的节点,如果key相同,那么表示存在相同节点,并且如果节点的类型也是一样的,那么就表示对比的这两个节点是同一个节点,这样的话,我们就只需要更新节点的props,返回这个更新后的节点。如果key不相同,那么表示当前对比的两个子节点不是相同节点,那么就直接返回null,就不需要进行节点的更新操作。

  • 判断返回的这个新的节点是否为null,如果为null就跳出整个循环,不会继续遍历后面的子节点。

  • 调用mapRemainingChildren方法,将第一次新节点和旧节点不相同的两个节点中的旧节点开始,及其后面的兄弟节点添加到一个map集合中,其中map中的key就是节点的key,value就是子节点。

  • 对新集合的节点进行遍历,调用updateFromMap方法,从map集合(旧集合)中匹配出新节点,如果能够匹配,那么就返回这个节点,再更新这个节点的props,并且删除map集合已经匹配的节点,如果旧集合中没有这个新节点,那么表示这个节点是新插入进来的节点。

  • 调用placeChild方法,对节点进行移动操作。在移动前需要将当前节点在旧集合中的位置与lastPlacedIndex进行比较,如果当前节点在旧集合的位置小于lastPlacedIndex,那么就需要进行移动,否则就不需要进行移动。这是一种顺序优化手段,lastPlacedIndex一直在更新,表示访问过的节点在旧集合中最右的位置(即最大的位置),如果新集合中当前访问的节点比lastPlacedIndex大,说明当前访问节点在旧集合中就比上一个节点位置靠后,则该节点不会影响其他节点位置,因此不需要进行移动,只有当访问的节点比lastPlacedIndex小时,才需要进行移动。

function placeChild(newFiber, lastPlacedIndex, newIndex) {
    // 更新新节点的index值
    newFiber.index = newIndex;
    if (!shouldTrackSideEffects) {
      // Noop.
        return lastPlacedIndex;
    }
    // 这个是当前新节点的旧节点
    var current$$1 = newFiber.alternate;
    // 如果旧节点存在,那么我们就能通过旧节点的index,获取旧节点在旧集合中的位置,并更新lastPlacedIndex的值(lastPlacedIndex的值始终都是访问过的节点在旧集合中的最右位置,即最大位置)
    // 如果旧节点不存在,表示是一个新创建的节点,那么就直接插入
    if (current$$1 !== null) {
        var oldIndex = current$$1.index;
        // 如果当前访问的节点在旧集合中的位置小于lastPlacedIndex,那么就需要进行移动操作
        // 否则,就不需要移动
        if (oldIndex < lastPlacedIndex) {
            newFiber.effectTag = Placement;
            return lastPlacedIndex;
        } else {
            // This item can stay in place.
            return oldIndex;
        }
    } else {
      newFiber.effectTag = Placement;
      return lastPlacedIndex;
    }
}

lastPlacedIndex的值相当于在旧集合中移动的最大值
image

上面这张图片分析的是新旧集合中存在相同节点但位置不同时,对节点进行位置移动的情况,如果新集合中有新加入的节点,那是怎么对比呢?以下面这个例子为例:

class App extends Component {
    constructor (props) {
        super(props);
        this.state = {
            list : ['andy' , 'jack' , 'henry']
        }
    }
    change = () => {
        this.setState({
            list : ['jack' , 'andy' , 'henry']
        })
    }
    insert = () => {
        this.setState({
            list : ['andy' , 'peter' , 'henry' , 'jack']
        })
    }
    render () {
        return (
            <div>
                <div>
                    {
                        this.state.list.map(item => (
                            <p key={item}>{item}</p>
                        ))
                    }
                </div>
                <button onClick={this.change}>change</button>
                <button onClick={this.insert}>insert</button>
            </div>
        )
    }
}

image

上面这张图Fenix的是新旧集合存在插入的新节点以及位置不同的情况。

如果新集合中有删除掉的节点,那react的diff是怎么做的呢?

其实比对的过程都是一样,只是多加了一步,就是会判断existingChildren这个map集合中是否还存在剩余的子节点,如果存在,那么表示这些子节点就是需要删除的,那么我们就调用deleteChild方法将子节点执行删除操作。

总结:

  • 对于tree diff来说,react只会对tree中同一层级进行比较,不同层级是不会比较的。
  • 对于component diff来说,react会判断component的类型是否相同,如果相同,那么就按照tree diff的方式来进行比较,如果component的类型不相同,那么就直接删除原来的component及其子节点,用新的component来代替。
  • 对于element diff来说,react会通过设置key来对element diff进行优化。

React服务端渲染

React服务端渲染

React用于编写spa应用非常的流行,但是这对于SEO很不友好,因为爬虫抓取不到页面的数据,页面的数据都是在页面加载完成之后通过请求获取并渲染到页面上的。所以爬虫抓到的只是一个空页面。那么如果对SEO优化有要求,我们可以使用React服务端渲染技术来实现。

React服务端渲染小demo

这里我们需要使用nodejs,webpack,babel来实现:

  • nodejs(nodejs作为中间层,主要作用是渲染html)
  • webpack(打包工具,v4.27.1版本)
  • babel(编译js和jsx)

ReactDOMServer类可以让你在服务端渲染你的组件,当我们传入一个React元素到这个类的renderToString()方法中,这样就会把React元素渲染为原始的html。然后我们就可以把html返回给浏览器。

import React , { Component } from 'react';
import ReactDOMServer from 'react-dom/server';
import express from 'express';
const app = express();
class App extends Component {
    constructor (props) {
        super(props);
    }
    render () {
        return (
            <div>
                <h1>hello andy</h1>
            </div>
        )
    }
};

const html = ReactDOMServer.renderToStaticMarkup(<App />);

app.get('/' , (req , res) => {
    res.write(`
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <meta http-equiv="X-UA-Compatible" content="ie=edge">
            <title>Document</title>
        </head>
        <body>
            ${html}
        </body>
        </html>
    `);
    res.end();
})

app.listen(3000 , () => {
    console.log('listening port 3000');
});

要想让babel能够识别并编译jsx语法,我们需要在babel的配置文件中(.babelrc)添加:

{
    "presets" : [
        "@babel/preset-env",
        "@babel/preset-react"
    ]
}

服务端渲染具体实现

如果需要实现服务端渲染,那么首先我们需要一个服务器,这里我使用express来搭建一个本地服务器。

import express from 'express';
import path from 'path';
const app = express();

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import Layout from './components/Layout';

// 静态文件位置
app.use(express.static('dist'));

app.get('/' , (req , res) => {
    const context = {};
    const html = ReactDOMServer.renderToString(
        <StaticRouter location={req.url} context={context}>
            <Layout />
        </StaticRouter>
    );
    res.write(`
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <meta http-equiv="X-UA-Compatible" content="ie=edge">
            <title>Document</title>
        </head>
        <body>
            <div id="app">${html}</div>
            <!-- 这里是打包后的客户端js代码 -->
            <script src="build.js"></script>
        </body>
        </html>
    `);
    res.end();
});

app.listen(3000 , () => {
    console.log('listening port 3000');
});

然后再创建一个webpack配置文件(webpack.config.server.js),主要是用来打包服务器的js代码

const path = require('path');
module.exports = {
    entry : './server.js',
    output : {
        path : path.resolve(__dirname),
        filename : 'server.build.js'
    },
    mode : 'development',
    target : 'node',
    devtool : '#source-map',
    module : {
        rules : [
            {
                test : /.(js|jsx)$/,
                use : 'babel-loader',
                exclude : /node_modules/
            }
        ]
    }
}

打包之后,我们可以执行打包后的文件,然后创建了一个本地服务器,监听3000端口。之后我们需要做的就是实现客户端代码。

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import Layout from './components/Layout';

const app = document.getElementById('app');

const jsx = (
    <Router>
        <Layout />
    </Router>
)
ReactDOM.hydrate(jsx , app);

然后,我们需要将客户端代码进行打包,新建一个webpack配置文件:webpack.config.client.js

const path = require('path');
module.exports = {
    entry : './index.js',
    output : {
        path : path.resolve(__dirname , './dist'),
        filename : 'build.js'
    },
    target : 'node',
    mode : 'development',
    devtool : '#source-map',
    module : {
        rules : [
            {
                test : /.jsx?$/,
                use : 'babel-loader',
                exclude : /node_modules/
            }
        ]
    }
};

然后我们可以使用webpack来打包输出我们需要的客户端和服务端的js代码,并执行服务端js代码,创建本地服务器。这里我们在package.json文件中配置:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "server" : "webpack --config webpack.config.server.js",
    "client" : "webpack --config webpack.config.client.js"
  }

所以我们只需要执行 npm run server 和 npm run client就可以分别打包客户端和服务端代码。

项目地址

React服务端渲染例子

项目展示

image

image

函数的bind方法

bind方法

bind方法,创建一个新函数,在调用这个函数时设置函数的this值,并将给定的参数列表作为原函数的参数列表的前若干项。该方法返回一个新函数。(mdn)

function.bind(thisArg[, arg1[, arg2[, ...]]])

当我们想要重新绑定一个函数的this值时,就可以使用bind来实现。传入的第一个参数就是在函数调用的时候需要绑定的this值,而其他参数就是在函数执行的时候作为原函数的参数放在函数列表前面,如果返回的新函数有传入的参数,那么会放在参数列表的后面。

var foo = {
    value : 1
}
function bar (name , age) {
    console.log(name);
    console.log(age);
    console.log(this.value)
};

var bindFoo = bar.bind(foo , 'andy');
bindFoo(12);

bind方法主要特点

  • 1、返回一个新函数
  • 2、绑定函数的this值
  • 3、可以传入参数
  • 4、当绑定函数作为构造函数调用时,指定的this值会失效,但是传入的参数会继续作为调用函数的参数,并且返回的对象的会继承构造函数的属性。

模拟实现bind方法

首先我们需要做的就是当调用函数的bind方法时,它会返回一个新函数,并且绑定this值。

Function.prototype.bind1 = function (context) {
    var self = this;
    // 返回一个新函数,并且通过apply方法来绑定函数的this值
    return function () {
        return self.apply(context);
    }
}

除此之外还可以传入参数,我们可以使用argumnets来实现

Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments , 1);
    return function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(context , args.concat(bindArgs));
    }
};

第四点的话,首先我们需要判断函数是正常调用还是通过new来调用,可以通过instanceof或者new.target来判断

Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments , 1);
    var BindFn = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof BindFn ? this : context , args.concat(bindArgs));
    };
    return BindFn;
};

当通过new来调用绑定的函数时,返回的对象会继承绑定函数的属性

Function.prototype.bind2 = function (context) {
    if (typeof this !== 'function') {
        throw new Error("一定要是函数");
    }
    var self = this;
    var args = Array.prototype.slice.call(arguments , 1);
    // 创建一个空构造函数
    var Noop = function () {};
    var BindFn = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof BindFn ? this : context , args.concat(bindArgs));
    };
    // 让空构造函数的原型指向原函数的原型,这样当new Noop()时,创建的对象会继承原函数的属性。
    // 原型继承来让BindFN的原型指向Noop对象,原型继承是这样的:BindFn对象——>Noop对象——>原函数prototype
    Noop.prototype = self.prototype;
    BindFn.prototype = new Noop();
    return BindFn;
};

react测试

react测试

测试react应用,主要就是测试react应用的各个功能是否都能正常使用。基本上一个功能都会涉及到以下几个部分:

  • action creator函数
  • 异步action creator函数
  • reducer函数
  • component组件

action creator测试

当我们编写完一个action creator函数,我们需要测试一下这个函数是否能返回我们想要的值。测试的时候,我们只需要设置一个期望的值,然后通过调用action creator函数返回的值进行比较,如果相同,那么表示action creator函数没有问题,如果不同,那么表示编写的action creator函数有问题。

// action.test.js
import { ADD_TODO , DELETE_TODO, addTodo, deleteTodo } from '../actions';
function addTodo (text) {
    return {
        type : ADD_TODO,
        text
    }
};
function deleteTodo (index) {
    return {
        type : DELETE_TODO,
        index
    }
};
describe('actions', () => {
    it('should create an action to add a todo', () => {
        const text = 'Finish docs'
        const expectedAction = {
            type: ADD_TODO,
            text
        }
        expect(addTodo(text)).toEqual(expectedAction)
    });
    it('should create an action to delete a todo' , () => {
        const index = 1;
        const expectedAction = {
            type : DELETE_TODO,
            index
        };
        expect(deleteTodo(index)).toEqual(expectedAction);
    })
})

异步action creator测试

对于异步action creator函数来说,虽然调用action creator函数返回的是一个函数,但是最终还是要dispatch一个同步的action,所以我们只需要测试同步的action creator函数返回的结果与我们自己设置的期望的结果是否相同,那么就能证明我们的异步action creator函数是否正常。

我们可以使用nock这个第三方库来模拟http请求响应。

通过使用redux-mock-store这个第三方库来mock出一个store,来测试异步aciton creator函数和中间件。

// asyncAction.test.js
import fetch from 'isomorphic-fetch'
import thunk from 'redux-thunk';
import nock from 'nock';
import expect from 'expect';
import configureMockStore from 'redux-mock-store'

const REQUEST_POSTS = 'REQUEST_POSTS';
const RECEIVE_POSTS = 'RECEIVE_POSTS';


function requestPosts () {
    return {
        type : REQUEST_POSTS
    }
};

function receivePosts (json) {
    return {
        type : json.type,
        success : json.success
    }
};

function fetchPosts (subreddit) {
    return (dispatch , getState) => {
        dispatch(requestPosts(subreddit));
        return fetch(`https://cnodejs.org/api/v1/topics`)
        .then(response => response.json())
        .then(json => {
            dispatch(receivePosts(json))
        })
        .catch(err => {
            console.log(err);
        })
    }
};


const middlewares = [ thunk ]
const mockStore = configureMockStore(middlewares);

describe('async actions' , () => {
    afterEach(() => {
        nock.cleanAll();
    })
    it('creates RECEIVE_POSTS when fetching data has been done' , () => {
        // 这里我们向https://cnodejs.org/api/v1/topics发送了一个get请求,这时nock会拦截这个请求,并立即返回我们预先定义好的响应这里的响应就是{success : true , type : REECEIVE_POSTS}
        nock(`https://cnodejs.org`)
        .get('/api/v1/topics')
        .reply(200 , {
            success : true,
            type : RECEIVE_POSTS
        })
        const expectedActions = [
            {type : REQUEST_POSTS},
            {type : RECEIVE_POSTS , success : true}
        ];
        const store = mockStore({});
        return store.dispatch(fetchPosts('reactjs'))
        .then(() => {
            expect(store.getActions()).toEqual(expectedActions);
        })
    });
});

Reducer函数测试

reducer函数的测试是比较简单的,我们可以这样:

// reducer.test.js
const ADD_TODO = 'ADD_TODO';
const DELETE_TODO = 'DELETE_TODO';
function addTodo (text) {
    return {
        type : ADD_TODO,
        text
    }
};

function deleteTodo (index) {
    return {
        type : DELETE_TODO,
        index
    }
};

function reducer (state = [] , action) {
    switch (action.type) {
        case ADD_TODO :
        return [...state , action.text];
        case DELETE_TODO :
        return [
            ...state.slice(0 , action.index),
            ...state.slice(action.index + 1)
        ];
        default :
        return state;
    }
};

describe('reducer函数测试' , () => {
    it('应该返回初始值' , () => {
        const expectValue = [];
        expect(reducer(undefined , {})).toEqual(expectValue);
    });

    it('添加一个代办事项' , () => {
        const expectValue = ['andy' , 'jack'];
        expect(reducer(['andy'] , addTodo('jack'))).toEqual(expectValue);
    });

    it('删除一个代办事项' , () => {
        const expectValue = ['jack' , 'peter'];
        expect(reducer(['andy' , 'jack' , 'peter'] , deleteTodo(0))).toEqual(expectValue);
    });
});

Component测试

组件测试,一般都是用来测试组件中的所有方法是否都会在满足相应的条件下执行,如果都执行了,那么说明这个组件的功能都是正常的。除此之外,我们还会去测试组件渲染是否都正常。component测试,我们可以使用enzyme库来进行测试。

// component.test.js
import React , { Component } from 'react';
import Enzyme , { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
class TodoTextInput extends Component {
    constructor (props) {
        super(props);
        this.handleKeyDown = this.handleKeyDown.bind(this);
        this.handleChange = this.handleChange.bind(this);
        this.state = {
            value : ''
        }
    }
    handleKeyDown (e) {
        if (e.keyCode === 13) {
            this.props.onSave(this.state.value);
        }
    }
    handleChange (e) {
        let value = e.target.value;
        this.setState({
            value
        })
    }
    render () {
        const { placeholder } = this.props;
        return (
            <div>
                <input placeholder={placeholder} onChange={this.handleChange} onKeyDown={this.handleKeyDown} />
            </div>
        )
    }
}

class MyComponent extends Component {
    constructor (props) {
        super(props);
        this.handleSave = this.handleSave.bind(this);
    }
    handleSave (text) {
        if (text.length !== 0) {
            this.props.addTodo(text);
        }
    }
    render () {
        return (
            <div className="header">
                <h1>todos</h1>
                <TodoTextInput newTodo={true} onSave={this.handleSave} placeholder="what needs to be done?" />
            </div>
        )
    }
};

function setup () {
    const props = {
        addTodo : jest.fn()
    };
    const enzymeWrapper = shallow(<MyComponent {...props} />);
    return {
        props,
        enzymeWrapper
    }
};

Enzyme.configure({ adapter: new Adapter() })

describe('components' , () => {
    describe('MyComponent' , () => {
        it("应该会选择组件本身以及子组件" , () => {
            const { enzymeWrapper } = setup();
            expect(enzymeWrapper.find('div').hasClass('header')).toBe(true);
            expect(enzymeWrapper.find('h1').text()).toBe('todos');
            const todoInputProps = enzymeWrapper.find('TodoTextInput').props();
            expect(todoInputProps.newTodo).toBe(true);
            expect(todoInputProps.placeholder).toBe('what needs to be done?');
        });
        it('如果text值的长度大于0,那么就会调用addTodo方法' , () => {
            const { enzymeWrapper , props } = setup();
            const input = enzymeWrapper.find('TodoTextInput');
            input.props().onSave('');
            expect(props.addTodo.mock.calls.length).toBe(0);
            input.props().onSave('andy');
            expect(props.addTodo.mock.calls.length).toBe(1);
        })
    })
});

react的首次渲染

react的首次渲染

参考这里

先看一个简单的例子:

class App extends Component {
    constructor (props) {
        super(props);
    }
    render () {
        return (
            <div>hello andy</div>
        )
    }
};

ReactDOM.render(<App /> , document.getElementById('root'));

当我们执行时,会在浏览器上渲染出hello andy的字样。那么React组件具体是怎么渲染的呢?虚拟DOM是怎么转换为真实DOM的?

当我们执行ReactDOM.render方法时,其实就是执行:

ReactDOM.render(
    React.createElement(App),
    document.getElementById('root')
)

通过调用React.createElement(App)创建了一个React元素。再将React元素渲染到root上。

我们先来看一下React组件在渲染过程中需要了解的一些数据结构。

1、ReactRoot

ReactRoot对象是是一个根对象。如下:

image

当我们得到一个ReactRoot对象后,然后调用ReactRoot对象的render方法进行渲染

// children是一个react元素
// callback是一开始调用ReactDOM.render()方法传入的回调函数
root.render(children , callback);

2、FiberRoot

FiberRoot是一个普通的对象,属于根对象。这个对象有几个重要的属性

  • 1、current:指向当前Fiber tree的根节点。
  • 2、containerInfo:指向React组件渲染到的容器,把整个React组件渲染到这个容器内。

3、FiberNode

FiberNode是一个FiberNode的实例,React的核心数据结构就是由多个FiberNode组件的一个FiberNode tree。

渲染准备阶段

1、创建ReactRoot,FiberRoot,FiberNode,并且建立它们与DOMContainer之间的联系

image

2、初始化(HostRoot)FiberNode的updateQueue

通过调用ReactRoot.render -> updateContainer -> updateContainerAtExpirationTime -> scheduleRootUpdate这一系列的函数,为这次初始化创建了一个update对象,然后把这个React元素作为update对象payload.element的值,然后把update放到(HostRoot)FiberNode的updateQueue上。

image

然后调用scheduleWork -> performSyncWork -> performWork -> performWorkOnRoot,期间主要是提取当前应该进行初始化的(HostFiber)FiberNode,后续正式进入算法执行阶段。

渲染执行阶段

调用renderRoot方法,生成一个完整的FiberNode tree。

1、生成(HostRoot)FiberNode的workInProgress,这个workInProgress就是current.alternate。

在整个算法过程中,主要做的事件就是遍历FiberNode节点。算法中有两个角色,一个是表示当前节点原始形态的current节点,一个是表示基于当前节点进行重新计算的workInProgress节点。两个对象实例都是独立的,互相之间通过alternate属性来引用。

image

2、循环执行

源码如下:

// 先创建workInProgress对象
nextUnitOfWork = createWorkInProgress(nextRoot.current, null, nextRenderExpirationTime);
//再调用workLoop函数
workLoop(isYieldy);
// 再循环执行performUnitOfWork
while (nextUnitOfWork !== null) {
  nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}

刚刚创建的FiberNode被作为nextUnitOfWork,从此进入工作循环,创建Fiber tree。

3、beginWork

每个工作的对象主要是处理workInProgress。这里通过workInProgress.tag区分当前的FiberNode类型,然后进行对应的更新处理。

3.1、updateHostRoot

通过FiberNode.tag来判断它的类型,如果是HostRoot,那么就会执行updateHostRoot方法,这个方法会执行两个操作,一个是处理更新队列,一个是创建FiberNode的child,得到下一个工作循环的传入的参数,也就是nextUnitOfWork = performUnitOfWork(nextUnitOfWork);

3.2、updateClassComponent

通过FiberNode.tag来判断它的类型,如果是ClassComponent,那么就会执行updateClassComponent方法,这个方法内部会调用ReactComponent的constructor来创建ReactComponent实例,并且会创建与FiberNode的关系。

image

调用constructor后(初始化React组件),会进入实例的挂载过程,这时候会把组件render之前的生命周期方法都调用完。期间,state可能会被以下流程修改:

  • 调用getDerivedStateFromProps
  • 调用componentWillMount
  • 处理因上面的流程产生的update所调用的processUpdateQueue

当创建完ReactComponent实例并与FiberNode建立联系后,通过调用ReactComponent实例的render方法获取子React元素,这里就是:

class App extends Component {
    constructor() {
      super();
      this.state = {
        msg:'init',
      };
    }
    render() {
      return (
        <div className="App">
          <p className="App-intro">
            To get started, edit <code>{this.state.msg}</code> and save to reload.
          </p>
          <button onClick={() => {
            this.setState({msg: 'clicked'});
          }}>hehe
          </button>
        </div>
      );
    }
}

子元素就是上面代码中的render方法返回的React元素。然后创建对应的所有子FiberNode。最终通过workInProgress.child指向第一个子FiberNode。

processUpdateQueue方法主要做的就是遍历这个updateQueue,然后计算出最后的新state,然后保存到workInProgress.memoizedState中。

3.3、reconcileChildren

在workInProgress节点自身处理完成之后,会通过props.children或者instance.render方法获取子React元素。子React元素可能是对象、数组、字符串、迭代器,针对不同的类型进行处理。

reconcileChildrenArray方法会遍历React元素数组,一一对应生成FiberNode,FiberNode有一个returnFiber属性和sibling属性,returnFiber指向其父FiberNode,sibling指向相邻的下一个兄弟FiberNode。最终生成的FiberNode tree结构为:

image

当生成FiberNode tree后,由于最后的那个FiberNode并没有child,那么就会调用completeUnitOfWork方法。

4、completeUnitOfWork

在completeWork方法中,会通过workInProgress.tag来区分不同的操作。我们这里来看一下HostText和HostComponent的操作

4.1、HostText

当workInProgress.tag为HostText,那么会执行updateHostText$1方法,在这个方法中会创建textNode,并将textNode保存在workInProgress.stateNode。而textNode的internalInstanceKey属性指向workInProgress,通过这样与自己的FiberNode建立联系。

image

4.2、HostComponent

在我们这个例子中,当处理完HostText之后,调度算法会寻找当前节点的sibling节点进行处理,所以会进入HostComponent处理流程中。

由于当前出于初始化流程,所以处理比较简单,只是根据FiberNode.tag(当前值是code)来创建一个DomElement,即通过document.createElement来创建节点。然后通过__reactInternalInstance$[randomKey]属性建立与自己的 FiberNode的联系;通过__reactEventHandlers$[randomKey]来建立与 props 的联系。

然后,通过setInitialProperties方法对DomElement的属性进行初始化,而节点的内容、样式、class、事件 Handler等等也是这个时候存放进去的。

目前,整个FiberNode tree结构如下:

image

在当前的DOM元素创建完成后,就会进入到appendAllChildren方法把子节点append到当前的DOM元素上。

while (node !== null) {
  if (node.tag === HostComponent || node.tag === HostText) {
    // 将子节点append到当前DOM中
    appendInitialChild(parent, node.stateNode);
  } else if (node.tag === HostPortal) {
    // If we have a portal child, then we don't want to traverse
    // down its children. Instead, we'll get insertions from each child in
    // the portal directly.
  } else if (node.child !== null) {
    // 如果有子节点
    node.child.return = node;
    node = node.child;
    continue;
  }
  if (node === workInProgress) {
    return;
  }
  // 循环遍历出子节点的所有兄弟节点
  while (node.sibling === null) {
    if (node.return === null || node.return === workInProgress) {
      return;
    }
    node = node.return;
  }
  node.sibling.return = node.return;
  node = node.sibling;
}
};

上面代码是appendAllChildren方法中的大部分代码,主要就是查找出当前DOM下的所有子节点,并将这些子节点append到当前DOM上。

最终,所有和DOM元素相关的FiberNode都被处理完了,得到一个最终的FiberNode tree结构:

image

提交阶段

提交阶段,主要就是执行一些周期函数,和DOM操作的阶段。

invokeGuardedCallback方法—>commitAllHostEffects方法—>然后根据effectTag来执行相应的操作(placement , update , deletion等)—>commitWork方法—>commitUpdate方法

commitUpdate方法

function commitUpdate(domElement, updatePayload, type, oldProps, newProps, internalInstanceHandle) {
    // 就是执行相应的更新操作
    updateFiberProps(domElement, newProps);
    updateProperties(domElement, updatePayload, type, oldProps, newProps);
}

updateProperties方法内部其实就是执行DOM操作。

JavaScript跨域访问

什么是跨域?

我自己理解的跨域访问就是指:当我们在访问一个资源的时候,只要协议名,域名,端口号三者中有一个不同,那么这就是跨域访问。

跨域的方式有哪些?

1、window.name

2、jsonp跨域

3、http代理(指的就是客户端发送请求到同域名的服务器,然后由服务器发送请求到指定的服务器上,并将获取的数据又返还给客户端)

4、CORS(跨域资源访问)

5、postMessage

6、websocket

window.name

window.name可以跨域,主要是因为window.name这个属性值可以存在不同的页面中,而且可以储存比较大的数据(2M)

// a.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <script>
        window.name = 'hello andy';
        setTimeout(function () {
            window.location.href = 'b.html';
        } , 2000)
    </script>
</body>
</html>
// b.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <script>
        console.log(window.name)
    </script>
</body>
</html>

当2秒中后,a.html页面跳转到b.html页面上,在b.html页面中,我们可以打印出a.html页面的name值。

那么怎么通过window.name就能在不跳转当前页面的时候也能访问其他页面的window.name值呢?可以使用iframe标签,该标签的src属性可以跨域访问资源,和script标签,img标签的src属性一样,它们是不受同域限制的,同时iframe本身就是当前页面的一个子页面,也同样具有window对象。这样我们是不是就可以通过访问子页面的window.name来访问外部的资源了呢?

// 后端是拿nodejs的express来搭建的本地服务器,这里就不写出来了,自己试一下就可以了
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>app1.html</title>
</head>
<body>

<script>
    var iframe = document.createElement('iframe');
    iframe.src = 'http://localhost:3000/data.html';
    document.body.appendChild(iframe);
    iframe.onload = function () {
        console.log(iframe.contentWindow.name);
    };
</script>

</body>
</html>
Uncaught DOMException: Blocked a frame with origin "http://localhost:8080" from accessing a cross-origin frame.

当这样访问的时候,直接报错了,因为如果我们需要读取iframe中的资源,也必须是同域才行。

所以我们可以在同域下面新建一个空白的页面,当iframe执行load事件的时候,将iframe的src属性指向该空白页面的路径,这样我们就可以在空白页面中获取到window.name,再通过当前页面来获取空白页面的window.name(因为此时已经同域环境下了),我们来试一下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>app1.html</title>
</head>
<body>

<script>
    var iframe = document.createElement('iframe');
    iframe.src = 'http://localhost:3000/data.html';
    document.body.appendChild(iframe);
    iframe.onload = function () {
        iframe.src = 'http://localhost:8080/proxy.html';
        console.log(iframe.contentWindow.name);
    };
</script>

</body>
</html>

这样就可以获取到了,但是iframe子页面一直在刷新,那是因为当触发load事件后,又重新设置src属性,如此一直会循环下去,所以页面一直都在刷新。我们可以通过一个变量来控制,最后的代码是这样的:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>app1.html</title>
</head>
<body>
<script>
    var iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    var state = false;
    iframe.onload = function () {
        if (state) {
            var data = iframe.contentWindow.name;
            console.log(data)
            iframe.contentWindow.document.write('');
            iframe.contentWindow.close();
            document.body.removeChild(iframe);
        } else {
            state = true;
            iframe.src = 'http://localhost:8080/proxy.html'
        }
    }
    iframe.src = "http://localhost:3000/data.html";
    document.body.appendChild(iframe);
</script>
</body>
</html>
// 跨域访问的页面
<script>
    window.name = 'andy';    
</script>

这样我们就能跨域获取到window.name的值了。这里需要注意的是,我们将跨域访问的数据保存在window.name中。

window.postMessage

首先我们需要去了解下这到底是个什么东西,可以查看来了解。

window.postMessage()方法可以安全的实现跨域通信。我们要知道这个window指的具体是什么?这个window一般指的是iframe的contentWindow属性,window.open返回的窗口对象,或者是window.iframes。具体用法可以mdn查询。

1、先通过window.open(url)来实现跨域

// proxy.html页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>proxy</title>
</head>
<body>
<script>
    var win = window.open('http://localhost:3000/data.html');
    // 这里一定要延迟执行postMessage方法,因为当我们调用window.open方法打开一个页面的,我们并不知道该页面什么时候会加载完,所以这里延迟执行,这样才不会报错,不然会报错的
    setTimeout(function () {
        win.postMessage('hello andy' , 'http://localhost:3000');
    } , 2000)
    window.addEventListener('message' , function (e) {
        console.log(e);
    } , false)
</script>
</body>
</html>
// data.html页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>data.html</title>
</head>
<body>
<script>
    window.addEventListener('message' , function (e) {
        e.source.postMessage('hello jack' , e.origin)
    } , false);
</script>
</body>
</html>

这样就可以打印出来事件对象了,其中传递的信息就包含在事件对象的data属性中。

2、另一种方式是通过iframe+window.postMessage的方式跨域,这种方式相对于前一种来说,它没有页面跳转,而且我们可以通过iframe的load事件来监听加载是否完成。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>proxy</title>
</head>
<body>
<iframe src="http://localhost:3000/data.html" id="iframe" frameborder="0"></iframe>
<script>
    var iframe = document.getElementById('iframe');
    iframe.onload = function () {
        iframe.contentWindow.postMessage('hello andy' , 'http://localhost:3000');
        window.addEventListener('message' , function (e) {
            console.log(e);
        } , false)
    }
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>data.html</title>
</head>
<body>
<script>
    window.addEventListener('message' , function (e) {
        e.source.postMessage('hello jack' , e.origin)
    } , false);
</script>
</body>
</html>

我们在通过window.postMessage的方式来跨域的时候,一定要检查origin属性,确定发送者的身份,不然很容易被攻击。所以一般代码都会这么写:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>proxy</title>
</head>
<body>
<iframe src="http://localhost:3000/data.html" id="iframe" frameborder="0"></iframe>
<script>
    var iframe = document.getElementById('iframe');
    iframe.onload = function () {
        iframe.contentWindow.postMessage('hello andy' , 'http://localhost:3000');
        window.addEventListener('message' , function (e) {
            if (e.origin != 'http://localhost:3000') {
                return;
            }
            console.log(e);
        } , false)
    }
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>data.html</title>
</head>
<body>
<script>
    window.addEventListener('message' , function (e) {
        if (e.origin != 'http://localhost:8080') {
            return;
        }
        e.source.postMessage('hello jack' , e.origin)
    } , false);
</script>
</body>
</html>

如果我们通过window.postMessage来发送ajax的post请求也是可以的,前面的方式不变,只要我们在另一个页面的message事件回调中再去发送ajax的post请求,将请求后的数据,再通过postMessage方法,传递给之前的页面即可。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>data.html</title>
</head>
<body>
<script>
    window.addEventListener('message' , function (e) {
        if (e.origin != 'http://localhost:8080') {
            return;
        };
        var res = null;
        var data = JSON.stringify(e.data);
        var xhr = new XMLHttpRequest();
        xhr.open('post' , '/data');
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4 && xhr.status == 200) {
                res = xhr.responseText;
                e.source.postMessage(res , e.origin)
            }
        };
        xhr.send(data);
    } , false);
</script>
</body>
</html>

前端异常捕获与上报

前端异常捕获与上报

一般来说我们捕获前端异常都是通过try/catch,当我们捕获到异常之后,再去处理异常,但是我们经常会看到后端会有一个错误日志,他们需要查看错误日志来确定具体是哪里出现异常,前端是不是也可以有一个错误日志呢?答案是肯定的。

try/catch

try/catch可以捕获同步代码的异常,但是对于异步代码的异常捕获就不行了,而且如果页面上存在太多的try/catch代码,也会影响代码的阅读。

// 同步代码异步捕获成功
try {
	var a = 1;
	var c = a + b;
} catch (err) {
	console.log(err)
}
// 异步代码异常捕获失败
try {
	setTimeout(function () {
		var a = 1;
		var c = a + b;
	} , 1000)
} catch (err) {
	console.log(err)
}

所以当是异步代码出现异常的时候,try/catch是很难捕获到的,如果没有及时捕获到异常,那么当我们去查找问题的时候,可能会花比较多的时间。那我们也可以换一种方式,通过window.onerror事件来全局捕获异常。

window.onerror

通过注册window.onoerror事件,来全局监听代码抛出异常,只要抛出异常,就会触发window.onerror回调,不管是同步代码还是异步代码。我们可以在代码的最前面注册window.onerror回调,如果将window.onerror的回调写在其他位置,可能在window.onerror之前代码抛出异常是不会触发该事件回调的。

var a = b + c;
window.onerror = function (errorMessage , scriptUrl , lineNo , columnNo , error) {
	console.log('errorMessage : ' + errorMessage);
	console.log('scriptUrl : ' + scriptUrl);
	console.log('lineNo : ' + lineNo);
	console.log('columnNo : ' + columnNo);
	console.log('error : ' + error);
}

上面代码是不会触发onerror事件回调的,因为当抛出异常的时候,window.onerror代码都还没有执行。

window.onerror = function (errorMessage , scriptUrl , lineNo , columnNo , error) {
	console.log('errorMessage : ' + errorMessage);
	console.log('scriptUrl : ' + scriptUrl);
	console.log('lineNo : ' + lineNo);
	console.log('columnNo : ' + columnNo);
	console.log('error : ' + error);
}
var a = b + c;
setTimeout(function () {
	var a = 1;
	var c = a + b;
} , 1000)

上面代码会触发事件回调,那么我们就可以获取到异常的具体信息,从而快速的修复异常问题。

当一个页面代码中存在多个script标签,那么只要在第一个script标签中注册window.onerror事件即可,其他script标签的代码执行的时候抛出的异常也会触发window.onerror事件回调。(注意:这里的script标签的代码都是在同域下)

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Document</title>
</head>
<body>
<script>
window.onerror = function (errorMessage , scriptUrl , lineNo , columnNo , error) {
	console.log('errorMessage : ' + errorMessage);
	console.log('scriptUrl : ' + scriptUrl);
	console.log('lineNo : ' + lineNo);
	console.log('columnNo : ' + columnNo);
	console.log('error : ' + error);
}
var a = b + c;
</script>
<script>
var a = b + c;
</script>
</body>
</html>

我们创建一个本地服务器,先来看一下在同域的情况下,会不会监听到加载的外部js文件的异常

// js代码
const express = require('express');
const app = express();
app.set('view engine' , 'html');
app.engine('html' , require('ejs').renderFile);
app.set('views' , './views');
app.use(express.static('static'));

app.get('/' , function (req , res) {
    res.render('./app1.html');
});

app.listen(3000 , () => {
    console.log('listening port 3000')
});
// html代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>app1.html</title>
</head>
<body>
<h1>app1.html</h1>
<script>
window.onerror = function (errorMessage , scriptUrl , lineNo , columnNo , error) {
	console.log('errorMessage : ' + errorMessage);
	console.log('scriptUrl : ' + scriptUrl);
	console.log('lineNo : ' + lineNo);
	console.log('columnNo : ' + columnNo);
	console.log('error : ' + error);
}
</script>
<script src="andy.js"></script>
</body>
</html>
// andy.js
dsfas

当我们打开本地服务器,发现是可以监听到的andy.js文件抛出的异常,也证明了在同域情况下window.onerror是可以做到全局监听js代码抛出的异常

当我们通过script标签获取的外部js文件不是同一个域名下的呢?是不是还是会监听到呢?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>app1.html</title>
</head>
<body>
<h1>app1.html</h1>
<script>
window.onerror = function (errorMessage , scriptUrl , lineNo , columnNo , error) {
	console.log('errorMessage : ' + errorMessage);
	console.log('scriptUrl : ' + scriptUrl);
	console.log('lineNo : ' + lineNo);
	console.log('columnNo : ' + columnNo);
	console.log('error : ' + error);
}
</script>
<script src="http://cdncss4.jobui.com/template_1/js/mobile/common1.js"></script>
</body>
</html>

我们发现在不同域名下window.onerror无法获取抛出的异常信息。解决的方式是给要请求的不同域名的js文件的script标签添加一个crossorigin属性,设置值为"anonymous",并且在服务端添加"Access-Control-Allow-Origin",一般cdn都是"*"

crossorigin="anonymous"
// 服务器配置
Access-Control-Allow-Origin:"*"

对于压缩的js代码,当抛出异常的时候,window.onerror获取到的错误信息都是在一行代码中,而且很难查找到具体的位置。有没有什么方法可以定位到具体的位置呢?我们可以使用sourceMap来实现。

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry : {
        app : './app.js'
    },
    output : {
        path : path.resolve(__dirname , 'dist'),
        filename : 'boundle.js'
    },
    mode : 'production',
    devtool : '#source-map',
    plugins : [
        new HtmlWebpackPlugin({
            title : 'webpack',
            filename : 'index.html',
            template : 'index.html'
        })
    ]
}

上面代码使用了source-map功能,所以当代码抛出异常的时候,浏览器也可以定位到具体的位置。

异常上报

我们可以通过nodejs作为中间层来将前端错误日报上报的nodejs中间层,然后再通过nodejs将错误日志发送到后端进行存储。从而完成日志上报的流程。

// 前端代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script>
        window.onerror = function (errorMessage , scriptUrl , lineNo , columnNo , error) {
            var errorObject = {
                errorMessage : errorMessage || null,
                scriptUrl : scriptUrl || null,
                lineNo : lineNo || null,
                columnNo : columnNo || null,
                statk : error && error.stack ? error.stack : null
            };
            var xhr = new XMLHttpRequest();
            xhr.open('post' , '/error');
            xhr.setRequestHeader('Content-Type' , 'application/json');
            xhr.send(JSON.stringify(errorObject));
        }
    </script>
</head>
<body>
    <h1>hello webpack</h1>
</body>
</html>
// 后端nodejs代码
const webpack = require('webpack');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const express = require('express');
const bodyParser = require('body-parser');
const webpackMiddleware = require('webpack-dev-middleware');
const app = express();
const options = {
    entry : {
        app : './app.js'
    },
    output : {
        path : path.resolve(__dirname , 'dist'),
        filename : 'boundle.js'
    },
    mode : 'production',
    devtool : '#source-map',
    plugins : [
        new HtmlWebpackPlugin({
            title : 'webpack',
            filename : 'index.html',
            template : 'index.html'
        })
    ]
};
const compiler = webpack(options);

app.use(webpackMiddleware(compiler , {
    publicPath : '/'
}));
app.use(bodyParser.json());

app.post('/error' , function (req , res) {
    // 可以在这里写错误日志上报的逻辑
});

app.listen(8080 , function () {
    console.log('listening port 8080')
})

React基本路由组件

react-router-dom

react路由功能,主要是通过react-router-dom库来实现。在React路由中,有三种类型的组件,一种是router组件,一种是route组件,一种是navigation组件。

router组件

router组件,每个react应用都应该有一个router组件,对于web项目来说,react-router-dom库提供了和两种router组件。这两个组件都会创建一个history对象。一般来说,如果你有服务器需要响应页面请求,那么你就使用组件,如果这是提供一些静态文件服务,那么就可以使用组件。

// 这里是使用BrowserRouter组件
import React, { Component } from 'react';
import { BrowserRouter as Router , Route , Link } from 'react-router-dom';
import './App.css';

const Home = () => <h2>Home</h2>;
const About = () => <h2>About</h2>;

const Header = () => (
    <ul>
        <li>
            <Link to="/">Home</Link>
        </li>
        <li>
            <Link to="/about">About</Link>
        </li>
    </ul>
)
// 这里需要注意的是,我们将BrowserRouter变量赋值为Router。
class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Header />
                    <Route path="/" component={Home} />
                    <Route path="/about" component={About} />
                </div>
            </Router>
        )
    }
}

结果如下:

结果图

// 这里是使用HashRouter组件
import React, { Component } from 'react';
import { HashRouter as Router , Route , Link } from 'react-router-dom';
import './App.css';

const Home = () => <h2>Home</h2>;
const About = () => <h2>About</h2>;

const Header = () => (
    <ul>
        <li>
            <Link to="/">Home</Link>
        </li>
        <li>
            <Link to="/about">About</Link>
        </li>
    </ul>
)
// 这里需要注意的是,我们将BrowserRouter变量赋值为Router。
class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Header />
                    <Route path="/" component={Home} />
                    <Route path="/about" component={About} />
                </div>
            </Router>
        )
    }
}

结果如下:

结果图

对比上面两种不同的路由方式,HashRouter组件是通过#后面那一部分的路径来展示不同的页面,而BrowserRouter组件则是我们经常使用的url来展示不同的页面,HashRouter路径改变,后端是不会响应url请求的,而BrowserRouter组件的路由地址改变,后端是会响应url请求的。

Route组件

有两种路由匹配组件,组件和组件。

1、Route组件会通过比较当前url地址的路径部分与组件的path属性是否相同来匹配,当一个组件匹配,那么就会渲染这个组件的内容,如果不匹配,那么就返回null。如果一个组件没有匹配路径,那么将匹配所有任何路径。

import React, { Component } from 'react';
import { BrowserRouter as Router , Route , Link , Switch , NavLink } from 'react-router-dom';

const Home = () => <h2>Home</h2>;
const About = () => <h2>About</h2>;
const NoMatch = () => <h2>No Match</h2>

const Header = () => (
    <ul>
        <li>
            <Link to="/">Home</Link>
        </li>
        <li>
            <Link to="/about">About</Link>
        </li>
    </ul>
)

class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Header />
                    <Route path="/" component={Home} />
                    <Route path="/about" component={About} />
                    <Route component={NoMatch} />
                </div>
            </Router>
        )
    }
}

上面代码中,我们可以知道,当url地址的路径与组件的path属性相匹配时,那么就会渲染对应的component属性指向的组件。当没有path属性的时候,任何时候都会匹配,渲染相应的组件。

image

大家应该留意到图片上,我只是匹配/about路径,但是home和noMatch都渲染出来了,这是为什么呢?因为所有的组件都会去匹配路径,如果匹配就渲染。所以"/"和"/about"是匹配的。

2、Switch组件,只会渲染第一个与路径相匹配的组件。其他是不会渲染的。这个对于我们多个路由同时匹配一个路径的时候就很有帮助,而且也可以再匹配不到任何路径的时候,展示404组件。

import React, { Component } from 'react';
import { BrowserRouter as Router , Route , Link , Switch , NavLink } from 'react-router-dom';
import './App.css';

const Home = () => <h2>Home</h2>;
const About = () => <h2>About</h2>;
const NoMatch = () => <h2>No Match</h2>

const Header = () => (
    <ul>
        <li>
            <Link to="/home">Home</Link>
        </li>
        <li>
            <Link to="/about">About</Link>
        </li>
    </ul>
)

class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Header />
                    <Switch>
                        <Route path="/about" component={About} />
                        <Route component={NoMatch} />
                    </Switch>
                </div>
            </Router>
        )
    }
}

如果我们想渲染组件的内容,我们可以通过三种方式,第一种是component属性指向一个react组件,第二种是render属性指向一个内联函数,第三种是使用children属性。

<Route path="/home" component={Home} />
<Route path="/about" render={props => <h2>About</h2>} />
<Route path="/user" children={User} />
Link组件

导航组件,其实就是被最终渲染成a标签,有两种Link组件,一种是组件,一种是组件,NavLink组件是一种特殊的Link组件,当点击时,组件会添加一个activeClassName的样式类,我们可以设置链接被点击后的样式。

import React, { Component } from 'react';
import { BrowserRouter as Router , Route , Link , Switch , NavLink } from 'react-router-dom';
import './App.css';

const Header = () => (
    <ul>
        <li>
            <Link to="/home">Home</Link>
        </li>
        <li>
            <Link to="/about">About</Link>
        </li>
        <li>
            <NavLink to="/react" activeClassName="aa">React</NavLink>
        </li>
    </ul>
);

const Home = () => <h2>Home</h2>;
const About = () => <h2>About</h2>;
const NoMatch = () => <h2>No Match</h2>;
const ReactDemo = () => <h2>ReactDemo</h2>;

const App = () => {
    return (
        <Router>
            <div>
                <Header />
                <Switch>
                    <Route path="/about" component={About} />
                    <Route path="/react" component={ReactDemo} />   
                    <Route component={NoMatch} />
                </Switch>
            </div>
        </Router>
    )
}

react动画

react动画

对于前端动画,估计是个前端应该都实现过,一般来说,css动画和js动画,我们使用的会比较多一点。css动画又可以分为transition动画和animation动画,而js动画,我们一般都使用定时器或者requestAnimationFrame方法来实现。其实实现react动画差不多也是用这些东西来实现。

通过css来实现react动画

transition动画
class App extends Component {
    constructor (props) {
        super(props);
        this.state = {
            left : 0
        }
    }
    add = () => {
        this.setState({
            left : this.state.left + 30
        })
    }
    reduce = () => {
        this.setState({
            left : (this.state.left - 30) <= 0 ? 0 : (this.state.left - 30)
        })
    }
    render () {
        const { left } = this.state;
        return (
            <div>
                <button onClick={this.add}>add left</button>
                <button onClick={this.reduce}>reduce</button>
                <div className="box" style={{left : `${left}px`}}></div>
            </div>
        )
    }
}

// css部分
.box {
    position: absolute;
    left: 0;
    top: 100px;
    width: 100px;
    height: 100px;
    background-color: #007aff;
    transition: left 0.3s ease;
}

通过定时器或者requestAnimationFrame来实现动画

class App extends Component {
    constructor (props) {
        super(props);
        this.state = {
            left : 0
        }
    }
    add = () => {
        let left = this.state.left;
        let destination = left + 20;
        let speed = (destination - left) / 300;
        let start = null;
        let animate = (timestamp) => {
            if (!start) {
                start = timestamp;
            }
            let duration = timestamp - start;
            let progress = Math.min(parseInt(speed * duration + left , 10) , destination);
            this.setState({
                left : progress
            })
            if (progress < destination) {
                requestAnimationFrame(animate);
            }
        }
        requestAnimationFrame(animate);
    }
    reduce = () => {
        let left = this.state.left;
        let destination = left - 20;
        let speed = (left - destination) / 300;
        let start = null;
        const animate = (timestamp) => {
            if (!start) {
                start = timestamp;
            }
            let duration = timestamp - start;
            let progress = Math.max(parseInt(left - speed * duration) , destination);
            this.setState({
                left : progress
            });
            if (progress > destination) {
                requestAnimationFrame(animate);
            }
        }
        requestAnimationFrame(animate);
    }
    render () {
        const {left} = this.state;
        return (
            <div>
                <button onClick={this.add}>add</button>
                <button onClick={this.reduce}>reduce</button>
                <div className="box" style={{left : `${left}px`}}></div>
            </div>
        )
    }
}

使用react-transition-group库来实现动画

1、使用CSSTransition来实现动画
class Alert extends Component {
    closeAlert = () => {
        const { close } = this.props;
        close();
    }
    render () {
        return (
            <div className="alert-box">
                <div className="alert-title">Animated alert message</div>
                <div className="alert-content">this alert message is being transitioned in and out of the DOM</div>
                <div className="alert-btn-box">
                    <button className="alert-btn" onClick={this.closeAlert}>Close</button>
                </div>
            </div>
        )
    }
}

class App extends Component {
    constructor (props) {
        super(props);
        this.state = {
            star : false
        }
    }
    handleClick = () => {
        this.setState({
            star : !this.state.star
        })
    }
    close = () => {
        this.setState({
            star : false
        })
    }
    render() {
        return (
            <div>
                <button onClick={this.handleClick}>click</button>
                <CSSTransition
                    in={this.state.star}
                    timeout={300}
                    classNames="star"
                    unmountOnExit
                >
                    <Alert close={() => this.close()} />
                </CSSTransition>
            </div>
        );
    }
}

css部分代码

.star-enter {
    opacity: 0;
    transform: scale(1.1);
}
.star-enter-active {
    opacity: 1;
    transform: scale(1);
    transition: all 300ms ease-out;
}
.star-exit {
    opacity: 1;
    transform: scale(1);
}
.star-exit-active {
    opacity: 0;
    transform: scale(1.1);
    transition: all 300ms ease-in;
}
.alert-box {
    position: absolute;
    top:40%;
    left:40%;
    background-color: #fff;
    border-radius: 4px;
    border: 1px solid #e4e4e4;
}
.alert-title {
    padding: 20px 15px 10px;
}
.alert-content {
    padding: 10px 15px;
}
.alert-btn-box {
    padding: 10px 15px 20px;
}
2、使用TransitionGroup来实现动画
class App extends Component {
    constructor (props) {
        super(props);
        this.state = {
            items : [
                {id : 1 , text : 'andy'},
                {id : 2 , text : 'jack'},
                {id : 3 , text : 'peter'},
            ]
        }
    }
    addItem = () => {
        const text = prompt('enter your name');
        console.log(this.state.items)
        if (text) {
            this.setState({
                items : this.state.items.concat({
                    id : new Date().getTime(),
                    text
                })
            })
        }
    }
    delete = (evt) => {
        const id = evt.target.dataset.id;
        this.setState({
            items : this.state.items.filter(item => {
                return item.id != id
            })
        })
    }
    render () {
        const {items} = this.state;
        return (
            <div>
                <TransitionGroup>
                    {
                        items.map(({id , text}) => (
                            <CSSTransition
                                key={id}
                                timeout={300}
                                classNames="item"
                            >
                                <div>
                                    <button data-id={id} onClick={this.delete}>X</button>
                                    {text}
                                </div>
                            </CSSTransition>
                        ))
                    }
                </TransitionGroup>
                <button onClick={this.addItem}>add item</button>
            </div>
        )
    }
}

css部分代码

.item-enter {
    opacity: 0;
}
.item-enter-active {
    opacity: 1;
    transition: all 0.3s ease;
}
.item-exit {
    opacity: 1;
}
.item-exit-active {
    opacity: 0;
    transition: all 0.3s ease;
}

数组去重

数组去重

关于这个问题,相信做过前端的同学都会遇到过,这可以说是一个经典的面试题了。

  • 1、双层循环去重
function deduplicate (arr) {
	var res = [];
	for (var i = 0 ; i < arr.length ; i++) {
		for (var j = 0 ; j < res.length ; j++) {
			if (arr[i] === res[j]) {
				break;
			}
		}
		if (j == res.length) {
			res.push(arr[i]);
		}
	}
	return res;
};

例子:

var arr = [1,7,2,3 ,4 ,1,5,3];
function deduplicate (arr) {
	var res = [];
	for (var i = 0 ; i < arr.length ; i++) {
		for (var j = 0 ; j < res.length ; j++) {
			if (res[j] === arr[i]) {
				break;
			};
		};
		if (j == res.length) {
			res.push(arr[i]);
		}
	};
	return res;
};
var aa = deduplicate(arr);
console.log(aa)
  • 2、利用对象键值对去重
function deduplicate (arr) {
	var res = [];
	var obj = {};
	for (var i = 0 , len = arr.length ; i < len ; i++) {
		if (!obj[arr[i]]) {
			obj[arr[i]] = true;
			res.push(arr[i])
		}
	};
	return res
};

例子:

var arr = [1,7,2,3 ,4 ,1,5,3];
function deduplicate (arr) {
	var res = [];
	var obj = {};
	for (var i = 0 , len = arr.length ; i < len ; i++) {
		if (!obj[arr[i]]) {
			obj[arr[i]] = true;
			res.push(arr[i])
		}
	};
	return res
};
var aa = deduplicate(arr);
console.log(aa)
  • 3、利用数组的indexOf方法去重
function deduplicate (arr) {
	var res = [];
	for (var i = 0 , len = arr.length ; i < len ; i++) {
		if (res.indexOf(arr[i]) == -1) {
			res.push(arr[i]);
		}
	}
	return res;
}

例子:

var arr = [1,7,2,3 ,4 ,1,5,3 , 'a' , 'a'];
function deduplicate (arr) {
	var res = [];
	for (var i = 0 , len = arr.length ; i < len ; i++) {
		if (res.indexOf(arr[i]) == -1) {
			res.push(arr[i]);
		}
	}
	return res;
}
var aa = deduplicate(arr);
console.log(aa)
  • 4、利用数组排序去重
function deduplicate (arr) {
	var res = [];
	var sortedArr = arr.slice().sort();
	var prev;
	for (var i = 0 , len = sortedArr.length ; i < len ; i++) {
		if (i == 0 || prev !== sortedArr[i]) {
			res.push(sortedArr[i]);
		}
		prev = sortedArr[i];
	}
	return res;
};

例子:

var arr = [1,7,2,3 ,4 ,1,5,3 , 'a' , 'a'];
function deduplicate (arr) {
	var res = [];
	var sortedArr = arr.slice().sort();
	var prev;
	for (var i = 0 , len = sortedArr.length ; i < len ; i++) {
		if (i == 0 || prev !== sortedArr[i]) {
			res.push(sortedArr[i]);
		}
		prev = sortedArr[i];
	}
	return res;
};
var aa = deduplicate(arr);
console.log(aa)
  • 5、利用es6的Set集合来去重
function deduplicate (arr) {
	var res = [...new Set(arr)];
	return res
}

例子:

var arr = [1,7,2,3 ,4 ,1,5,3 , 'a' , 'a'];
function deduplicate (arr) {
	var res = [...new Set(arr)];
	return res
}
var aa = deduplicate(arr);
console.log(aa)
  • 6、利用es6的Map集合来去重
function deduplicate (arr) {
	var map = new Map();
	return arr.filter(function (item) {
		if (!map.has(item)) {
			map.set(item , 1);
			return true;
		} else {
			return false;
		}
	});
}

例子:

var arr = [1,7,2,3 ,4 ,1,5,3 , 'a' , 'a'];
function deduplicate (arr) {
	var map = new Map();
	return arr.filter(function (item) {
		if (!map.has(item)) {
			map.set(item , 1);
			return true;
		} else {
			return false;
		}
	});
}
var aa = deduplicate(arr);
console.log(aa)

总结:

1、 如果我们需要去重的数组是经过排序后的数组,那么我们这样使用:

function deduplicate (arr , isSorted) {
	var res = [] , prev;
	for (var i = 0 , len = arr.length ; i < len ; i++) {
		if (isSorted) {
			if (i == 0 || prev !== arr[i]) {
				res.push(arr[i]);
			}
			prev = arr[i];
		} else if (res.indexOf(arr[i]) == -1) {
			res.push(arr[i]);
		}
	}
	return res;
};

2、 如果我们需要先将去重的数组的每个元素进行处理后,再去重的话,我们可以这样使用:

function deduplicate (arr , isSorted , iterate) {
	var res = [] , computed = [] , prev;
	if (typeof isSorted == 'function') {
		iterate = isSorted;
		isSorted = false;
	};
	for (var i = 0 , len = arr.length ; i < len ; i++) {
		var value = arr[i];
		var computeValue = iterate ? iterate(value , i , arr) : value;
		if (isSorted) {
			if (i == 0 || prev !== value) {
				res.push(value);
			}
			prev = value;
		} else if (iterate) {
			if (computed.indexOf(computeValue) == -1) {
				computed.push(computeValue);
				res.push(value);
			}
		} else if (res.indexOf(value) == -1) {
			res.push(value);
		}
	};
	return res;
};

例子:比如我们要将数组中的'andy'和'Andy'两个元素看成是一样的,那么就可以:

var arr = [1,7,2,3 ,4 ,1,5,3 , 'andy' , 'Andy'];
function deduplicate (arr , isSorted , iterate) {
	var res = [] , computed = [] , prev;
	if (typeof isSorted == 'function') {
		iterate = isSorted;
		isSorted = false;
	};
	for (var i = 0 , len = arr.length ; i < len ; i++) {
		var value = arr[i];
		var computeValue = iterate ? iterate(value , i , arr) : value;
		if (isSorted) {
			if (i == 0 || prev !== value) {
				res.push(value);
			}
			prev = value;
		} else if (iterate) {
			if (computed.indexOf(computeValue) == -1) {
				computed.push(computeValue);
				res.push(value);
			}
		} else if (res.indexOf(value) == -1) {
			res.push(value);
		}
	};
	return res;
};
var aa = deduplicate(arr , function (item , index , arr) {
	return typeof item == 'string' ? item.toLowerCase() : item;
});
console.log(aa)

浏览器的事件循环机制

浏览器的事件循环机制(未完待续)

事件循环,其实是浏览器为了解决JavaScript单线程运行时不会阻塞的一种机制。

JavaScript的内存机制

内存机制

栈内存

在JavaScript中,栈内存一般用来储存基本数据类型

Number,String,Null,Boolean,Undefined等

比如:

var a = 10;
var b = 'hello andy';

这样简单的赋值操作就是将基本类型数据直接保存在栈内存中,我们可以直接操作保存在栈内存中的值,所以基本数据类型都是按值访问。

var a = 1;
var b = a;
b = 2;
console.log(a);  // 结果为:1

堆内存

在JavaScript中,堆内存一般用来储存引用数据类型

Array,Object等

比如:

var a = {name : 'andy'}
var b = [1 , 2 , 3];

引用类型的值被保存在堆内存中,JavaScript不能直接访问堆内存中的值,当我们需要访问堆内存中的值时,我们首先是访问保存在栈内存中指向堆内存中的值的引用,然后通过引用来访问堆内存中的值。

内存机制

Arguments对象

Arguments对象

在函数体中有一个隐式参数:arguments,arguments表示的是一个对应于传递给函数的参数的类数组对象。

{
    0 : 'andy',
    1 : 'jack',
    length : 2
}

arguments并不是一个真正的数组,只是一个类数组对象,它只有length属性和索引元素,没有数组其他的方法,比如:push,pop等。我们可以通过索引下标来读写arguments对象中的属性值。

arguments中保存的是函数实参,而不是函数形参:

function foo (a , b , c) {
    console.log(arguments);
    console.log(arguments.length);  // 2
}
foo(1,2);
// 打印的结果是2,而并不是3

Arguments对象的属性

  • length属性:
    • 表示的是接受函数实参的个数
  • callee属性:
    • 表示的是当前正在执行的函数
function add () {
    var sum = 0;
    var len = arguments.length;
    for (var i = 0 ; i < len ; i++) {
        sum += arguments[i];
    };
    return sum;
}

console.log(add(1 , 2 , 3 , 4));
console.log(add(1 , 2 , 3));
function foo (x) {
    if (x < 2) {
        return x;
    }
    return x * arguments.callee(x - 1);
}
console.log(foo(5));

arguments与对应的参数绑定

在非严格模式下,arguments对象中的元素会与实参的值共享,只要其中一方的值发生改变,另一方也会跟着一起改变,如果在严格模式下,则不会共享。

function foo (a , b , c) {
    console.log(arguments[0] , a);
    console.log(arguments[1] , b);
    a = 3;
    console.log(arguments[0] , a);
    arguments[0] = 4;
    console.log(arguments[0] , a);
    console.log(arguments[2] , c);
    c = 5;
    console.log(arguments[2] , c);
}
foo(1,2);

结果为:

1 1
2 2
3 3
4 4
undefined undefined
undefined 5
'use strict';
function foo (a , b , c) {
    console.log(arguments[0] , a);
    console.log(arguments[1] , b);
    a = 3;
    console.log(arguments[0] , a);
    arguments[0] = 4;
    console.log(arguments[0] , a);
    console.log(arguments[2] , c);
    c = 5;
    console.log(arguments[2] , c);
}
foo(1,2);

结果为:

1 1
2 2
1 3
4 3
undefined undefined
undefined 5

传递参数

将arguments从一个函数传递到另一个函数,我们可以使用函数的apply方法

function foo () {
    bar.apply(this , arguments);
}

function bar (a , b , c) {
    console.log(a , b , c);
}

foo(1,2,3);

Arguments对象转数组

  • 通过数组的slice方法来实现,Array.prototype.slice.call(arguemnts)或者[].slice.call(arguments)
  • 使用Array.from方法,该方法可以传入一个类数组对象。Array.from(arguments)
  • 使用扩展运算符
  • 定义一个数组,遍历arguments对象,然后将每一个元素重新保存到数组中,并返回
function foo () {
    var args1 = Array.prototype.slice.call(arguments);
    console.log(args1);
    var args2 = [].slice.call(arguments);
    console.log(args2);
    var args3 = Array.from(arguments);
    console.log(args3);
    var args4 = Array.prototype.concat.apply([] , arguments);
    console.log(args4)
}
foo(1,2,3);
// 使用扩展运算符
function bar (...arguments) {
    console.log(arguments);
}

bar(2,3,4);
// 定义一个数组,遍历arguments对象,然后将每一个元素重新保存到数组中,并返回
function foo () {
    var len = arguments.length;
    var arr = [];
    for (var i = 0 ; i < len ; i++) {
        arr[i] = arguments[i];
    }
    return arr;
}

判断类数组对象

// 字符串和函数具有length属性,我们可以通过typeof来判断obj是否为object类型
// 基本上就是判断obj是否具有length属性,以及length的值是否为一个正整数
function isArrayLike (obj) {
    if (obj && typeof obj === 'object' && isFinite(obj.length) && obj.length >= 0 && obj.length === Math.floor(obj.length) && obj.length < 2^32) {
        return true;
    } else {
        return false;
    }
};

应用

1、函数柯里化

function currying (fn) {
    var args = Array.prototype.slice.call(arguments , 1);
    return function () {
        return fn.apply(this , args.concat(Array.prototype.slice.call(arguments)));
    }
}
function add (a, b) {
    return a + b;
}
var bar = currying(add , 1);
console.log(bar(2))

2、递归调用

function fibonacci (n) {
    if (n == 1 || n == 2) {
        return 1;
    }
    return arguments.callee(n - 1) + arguments.callee(n - 2);
}
console.log(fibonacci(5));


function fn (x) {
    if (x == 1) {
        return 1;
    }
    return x * arguments.callee(x - 1)
};

console.log(fn(4));

3、函数重载(具有相同的函数名,不同的参数列表的函数)

// 可以通过参数数量的不同,执行不同的操作
function fn () {
    switch (arguments.length) {
        case 0 :
        // 执行相关操作
        break;
        case 1 :
        // 执行相关操作
        break;
        case 2 : 
        // 执行相关操作
        break;
    }
}

4、不定长参数

剩余参数,默认参数和解构赋值参数

arguments可以和剩余参数,默认参数和解构赋值参数结合一起使用。

在严格模式下,剩余参数,默认参数和解构赋值参数的存在不会影响arguments的行为,但是在非严格模式下,就会有所不同。

在非严格模式下,函数没有包含剩余参数,默认参数和解构赋值参数,那么arguments中的值会跟踪参数的值,比如:

// 当我们不管是修改函数参数的值还是修改arguments对象的值,都会跟着一起改变
function fn1 (a) {
    a = 2;
    console.log(arguments[0])   // 2
}
fn1(1);

function fn2 (a) {
    arguments[0] = 2;
    console.log(a);      // 2
}
fn2(1);

在非严格模式下,函数有包含剩余参数,默认参数和解构赋值参数,那么arguments中的值不会跟踪参数的值,比如:

// 函数包含默认参数,当我们需修改参数的值时,arguments中的值不会跟着改变
function fn1 (a = 1) {
    a = 2;
    console.log(arguments[0])    // 3
}
fn1(3);

// 或者
function fn2 (a = 1) {
    arguments[0] = 2;
    console.log(a);    // 3
}
fn2(3);

// 或者
function fn3 (a = 1) {
    console.log(arguments[0])   // undefined
}
fn3();

函数柯里化

函数柯里化

什么是柯里化?

柯里化:是把接受多个参数的函数变换为接受一个单一参数的函数,并且返回接受余下的参数而且返回结果的新函数技术。

举个例子:

// 这是一个正常函数的调用
function add (a , b) {
    return a + b;
};
add(1 , 2);

// 将上面的函数进行柯里化,则变为
function curry (a) {
    return function () {
        var args = Array.prototype.slice.call(arguments);
        return a + args[0];
    }
}
curry(1)(2)

通过函数柯里化定义,如果我们传入柯里化函数的第一个参数为一个函数,那会变成什么样子呢?

function add (a , b) {
	return a + b;
}

function curry (fn) {
	var args = [].slice.call(arguments , 1);
	return function () {
		var newArgs = args.concat([].slice.call(arguments));
		return fn.apply(this , newArgs);
	}
};

var fn = curry(add , 2);
console.log(fn(4));
// 或者
var fn = curry(add);
console.log(fn(4 , 2));
// 或者
var fn = curry(add , 2 , 3);
console.log(fn());

实现

let curry = function (fn) {
    let args = Array.prototype.slice.call(arguments , 1);
    return function () {
        let newArgs = args.concat(Array.prototype.slice.call(arguments));
        return fn.apply(this , newArgs);
    }
};
let curryAdd1 = curry(add , 1 , 2);
console.log(curryAdd1())

let curryAdd2 = curry(add , 1);
console.log(curryAdd2(2));

let curryAdd3 = curry(add);
console.log(curryAdd3(1 , 2));

通过上面的代码,我们能够实现返回的函数的一次调用并传入相应的参数,而且curry函数中可以传入一个函数和调用该函数需要的参数,如果我们能够实现函数这样调用那就比较完美了:

fn(1)(2)
// 现在的调用是fn(1 , 2)

如果要实现上述功能,那么当每次调用fn(x)的时候都返回一个函数,直到传入的参数与需要进行柯里化的函数的参数个数一致。其实我们可以理解为,每调用一次fn(x),内部保存了一个数组,就将变量添加到这个数组中,然后通过数组的长度去与需要柯里化函数的参数的个数做比较,比如:

function currying (fn , args) {
	var len = fn.length;
	args = args || [];
	return function () {
		var _args = [].slice.call(args);
		for (var i = 0 ; i < arguments.length ; i++) {
			var arg = arguments[i];
			_args.push(arg);
		}
		if (_args.length < len) {
			return currying.call(this , fn , _args);
		} else {
			return fn.apply(this , _args);
		}
	}
};
function add (a , b) {
	return a + b;
}
var fn = currying(add);
console.log(fn(1)(2));
// 结果为:3

我们可以这样来实现,从上面的调用,我们可以看到,当调用fn(1)的时候,返回的是一个函数,然后再调用fn(2)的时候,返回的也还是一个函数,然后再调用fn(3)的时候,返回的就是结果。

function second_curry (fn) {
    let args = Array.prototype.slice.call(arguments , 1);
    return function () {
        let newArgs = args.concat(Array.prototype.slice.call(arguments));
        return fn.apply(this , newArgs);
    }
};

function curry (fn , len) {
    len = len || fn.length;
    return function () {
        if (arguments.length < len) {
            let arr = [fn].concat(Array.prototype.slice.call(arguments));
            return curry(second_curry.apply(this , arr) , len - arguments.length);
        } else {
            return fn.apply(this , arguments);
        }
    }
}
function add (a , b , c) {
    return a + b + c;
};
let fn = curry(add);
console.log(fn(1)(3)(4))

通过上面代码,可以正确执行,其实调用的次数就是根据形参的个数来判断的。

反柯里化

Function.prototype.unCurrying = function () {
    var self = this;
    return function () {
        var obj = Array.prototype.shift.call(arguments);
        return self.apply(obj , arguments);
    }
}
Function.prototype.unCurrying = function () {
    var self = this;
    return function () {
        var obj = Array.prototype.shift.call(arguments);
        return self.apply(obj , arguments);
    }
}

var push = Array.prototype.push.unCurrying();
var obj = {
    '0' : 1,
    length : 1
};
push(obj , 2 , 3 , 4);
console.log(obj)

React之store介绍

Redux之reducer函数

reducer是一个纯函数,主要作用就是用来修改状态的。当数据状态比较复杂的时候,我们可以编写多个子reducer分别处理对应的部分数据状态,然后再将多个子reducer合并成一个reducer。

根据redux约定,reducer是一个纯函数,并且函数里面不包含任何副作用。这样就能保证状态的可预测和可追踪性。在reducer函数中是通过action的类型来分别对状态做对应的修改操作。

这里需要注意的是:如果状态只是基本数据类型,那么可以直接修改状态,因为返回的修改后的值是一个新的状态值,而不会对之前的状态进行修改。如果状态是一个引用类型,那么我们在修改状态的时候,就一定要注意不能改变原来的状态。

reducer函数是由开发者自己编写,一般格式都是:

function reducer (prevState , action) {
    switch (aciton.type) {
        case1 :
        return state1;
        ...
        caseN :
        return stateN;
        default : 
        return prevState;
    }
}

如何编写多个子reducer并合成一个reducer

首先我们需要明确的是子reducer其实就是一个普通的reducer函数,在写法上是类似的,然后我们通过combineReducers方法进行合并,这个方法接受一个对象作为参数,对象的键指的就是状态的变量名,对象的值指的就是子reducer函数。当我们dispatch一个action时,就会调用所有这些子reducer函数。

const reducer1 = function (state = 0 , action) {
    switch (action.type) {
        case 'PLUS' : 
        return state + 1;
        case 'MINUS' :
        return state - 1;
        default :
        return state;
    }
};
const reducer2 = function (state = [] , action) {
    switch (action.type) {
        case "ADD" : 
        return state.concat([action.data]);
        case "DELETE" :
        return state.slice(0 , action.index).concat(state.slice(action.index+1));
        default :
        return state;
    }
}
const store = createStore(combineReducers({
    data1 : reducer1,
    data2 : reducer2
}));
store.subscribe(function () {
    console.log(store.getState());
})
store.dispatch({
    type : 'PLUS'
});
store.dispatch({
    type : 'ADD',
    index : 0,
    data : 'andy'
});
store.dispatch({
    type : 'ADD',
    index : 1,
    data : 'jack'
});
store.dispatch({
    type : 'DELETE',
    index : 0
});

上面代码中,当我们在处理状态为引用类型时,一定要注意不能修改原状态值。

combineReducers内部原理

我们来看一下redux的combineReducers部分的源码是怎么实现的

// 这里传递的reducers参数是一个对象
function combineReducers(reducers) {
    // 获取对象所有的键名
    var reducerKeys = Object.keys(reducers);
    var finalReducers = {};
    
    // 遍历键名,验证键名是否存在
    // 如果存在那么就将reducer函数与键名建立映射关系
    for (var i = 0; i < reducerKeys.length; i++) {
        var key = reducerKeys[i];
    
        if (process.env.NODE_ENV !== 'production') {
          if (typeof reducers[key] === 'undefined') {
            warning("No reducer provided for key \"" + key + "\"");
          }
        }
        
        if (typeof reducers[key] === 'function') {
          finalReducers[key] = reducers[key];
        }
    }

    var finalReducerKeys = Object.keys(finalReducers);
    var unexpectedKeyCache;

    if (process.env.NODE_ENV !== 'production') {
        unexpectedKeyCache = {};
    }

    var shapeAssertionError;
    
    // 这个是验证reducer函数结构是否正确
    try {
        assertReducerShape(finalReducers);
    } catch (e) {
        shapeAssertionError = e;
    }
    
    // 返回一个函数,这个函数会被传入到createStore()方法中
    // 当我们调用store.createStore()方法创建一个store时,就会被调用
    return function combination(state, action) {
        // state变量是一个初始state,主要用来与nextState进行比较,如果两个不一样,那么表示上一个状态和当前状态是不一样的,那么就表示状态发生了改变
        if (state === void 0) {
            state = {};
        }
        // 如果reducer函数结构有问题,那么就抛出错误
        if (shapeAssertionError) {
          throw shapeAssertionError;
        }
    
        if (process.env.NODE_ENV !== 'production') {
          var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache);
        
          if (warningMessage) {
            warning(warningMessage);
          }
        }
        
        // state是否有改变
        var hasChanged = false;
        // 下一个状态,用来与state做比较
        var nextState = {};
        
        // 遍历所有的键名,调用所有reducer函数,并将reducer函数返回的结果保存在nextState变量中
        // 判断两个state的值是否一样,如果不一样,那么表示状态发生改变,则hasChaged为true,否则,为false
        for (var _i = 0; _i < finalReducerKeys.length; _i++) {
            var _key = finalReducerKeys[_i];
            var reducer = finalReducers[_key];
            var previousStateForKey = state[_key];
            var nextStateForKey = reducer(previousStateForKey, action);
            
            if (typeof nextStateForKey === 'undefined') {
                var errorMessage = getUndefinedStateErrorMessage(_key, action);
                throw new Error(errorMessage);
            }
            
            nextState[_key] = nextStateForKey;
            hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
        }
    
        return hasChanged ? nextState : state;
    };
}

小结:

- reducer函数必须是一个纯函数,并且函数里面是不能有任何副作用的。

- 可以将一个复杂的reducer函数,分割成多个子reducer函数,每个子reducer函数负责一部分状态的修改,然后再通过combineReducers方法将多个子reducer函数合并成一个reducer函数。

css加载会造成阻塞吗?

css加载会造成阻塞吗?

我们可以先来看一下css的加载对DOM的解析和渲染有没有影响,举个例子来测试一下:首先我们要把谷歌浏览器的的下载速率调低,然后通过link标签引入一个css文件,文件尽量大一些,这样更容易看到效果。

// html代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        #box {
            color: red;
        }
    </style>
    <script>
        function foo () {
            console.log(document.getElementById('box'));
        }
        setTimeout(foo , 0);
    </script>
    <link rel="stylesheet" href="./index.css">
</head>
<body>
    <div id="box">hello andy</div>
<script>
</script>
</body>
</html>

测试结果是:当我们访问这个页面的时候,一开始页面是空白的,等到css文件加载完成之后,才会显示“hello andy”,但是我们在控制台可以看到,就算页面是空白的情况,js也能获取到box节点。

结论就是:所以这就说明了,css文件的加载会不会阻塞DOM树的解析,但是会阻塞DOM树的渲染。

css加载会不会阻塞js代码的执行呢?我们可以做一个小实验:

html代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        #box {
            color: red !important;
        }
    </style>
    <script>
        console.log('加载css文件前执行');
        let start = Date.now();
    </script>
    <link rel="stylesheet" href="./index.css">
</head>
<body>
    <div id="box">hello amdy</div>
    <script>
        let end = Date.now();
        console.log('加载css文件后执行');
        let diff = end - start;
        console.log(`已经过了${diff}ms`);
    </script>
</body>
</html>

上面代码中,我们可以在link标签前面写一段js代码,然后再link标签后面写一代js代码,当访问页面的时候,我们发现浏览器的控制台会先打印出“加载css文件前执行”,然后等到css文件加载之后,会打印出“加载css文件后执行”,而且我们也可以得出,经过了3秒多之后才执行js代码,这也就是说,css加载也会阻塞js代码执行。

结论就是:css加载会阻塞js代码执行

结论

  • css加载不会阻塞DOM树的解析
  • css加载会阻塞DOM树的渲染
  • css加载会阻塞css文件后面的js代码的执行

如果我们知道了css加载会影响这些,那么如果我们想提高页面渲染的速度,或者说是减少页面白屏的时间,那么我们就要尽可能的提高css的加载速度。比如可以使用下面的方法:

  • 1、使用CDN
  • 2、压缩css文件
  • 3、使用http缓存
  • 4、减少http请求数,可以将多个css文件,合并成一个。

为什么会出现这样的情况呢?

webkit渲染过程:

image

从上面的图片中可以看出,webkit的渲染过程是这样的:

  • 解析html,构建DOM树
  • 解析css,构建CSSOM树
  • 将DOM树和CSSOM树合并成渲染树
  • 技术可见节点的几何信息
  • 绘制

我们可以发现,其实构建DOM树和构建CSSOM树是并行的过程,这也就解释了为什么CSS加载不会阻塞DOM树的解析。而渲染树是依赖DOM树和CSSOM树的,只有DOM树和CSSOM树都构建完成之后,才能合成渲染树,所以它必须等待CSSOM树构建完成,也就是等到CSS文件加载完成之后,才能开始渲染。所以CSS加载会阻塞DOM的渲染。

参考这里,虽然写的很详细,但是自己还是想亲自操作一遍,这样也能加深理解。

http代理

http代理

http代理,对于大部分程序员来说应该都是用过的,而作为前端,我一般用它来进行跨域访问。当然http代理的功能有很多,比如拦截,监控,过滤,缓存等这些都可以通过http代理来实现。

什么是http代理?

http代理,对于客户端和服务器来说就是一个中间人的角色,对于连接它的客户端来说,它是服务器,对于它连接的服务器来说,它是客户端,而http代理主要就是来回传送客户端和服务器之间的http报文。

http代理有什么功能?

http代理可以实现的功能比较多,比如:拦截,监控,过滤,web缓存,跨域,反向代理等。这些功能都可以通过代理来实现。

1、拦截功能:

比如我们需要拦截用户登录,如果用户已经登录,那么就直接返回页面,如果用户还没有登录,就重定向到用户登录页面,引导用户登录。或者页面存在权限控制,针对不同权限返回不同页面信息。

2、监控功能:

比如我们需要监控代码异常情况进行上报,那么我们需要监控特定的请求接口,如果有请求,我们就将异常进行上报。

3、过滤功能:

比如我们可以设置有些人可以访问我们的网站,有些人不能访问我们的网站。

4、web缓存功能:

每次请求一个页面时,代理服务器都会检查这个页面是否已经缓存在本地,如果在,就直接返回页面,如果不在,那么再向目标服务器发送请求获取页面,并缓存在代理服务器本地。

5、跨域功能:

我们经常需要跨域访问一些服务器上的数据,这个时候我们可以在同域下创建一个代理服务器,然后由代理服务器去请求不同域名下的服务器上的数据。

6、反向代理功能:

当我们访问一个网址,其实nginx会接收到我们的请求,然后由nginx再转发给其他服务器,其中nginx就实现了反向代理的功能,对于客户端来说,并不知道是由哪一台服务器来返回页面的。

正向代理和反向代理的区别

http代理可以分为正向代理和反向代理,它们之间的区别主要在于代理的对象不一样,正向代理,代理的对象是客户端,主要是为客户端收发请求,而反向代理,代理的是服务端,主要是为服务端收发请求。

简单例子:
const http = require('http');
const url = require('url');

http.createServer(function (req , res) {
    let proxyHeaders = {
        'X-Forwarded-For' : 'localhost',
        "X-Forward-IP" : req.connection.remoteAddress
    };
    let proxyClient = http.request({
        hostname : 'localhost',
        port : 8080,
        headers : proxyHeaders
    });
    req.pipe(proxyClient);
    proxyClient.on('response' , function (proxyResponse) {
        proxyResponse.pipe(res);
    });
}).listen(3000);
console.log('listening port 3000');

http隧道代理

客户端先发送connect请求到隧道代理服务器,告诉隧道代理服务器,建立和服务器的TCP连接,代理服务器成功和后端建立连接后,代理服务器会返回“HTTP/1.1 200 Connection Established”报文,告诉客户端连接已经成功建立,这个时候建立了连接,所以发给代理服务器的TCP报文都会直接转发到目标服务器,从而实现了客户端和服务器的通信。
客户端代码:

var http = require('http');
var https = require('https');
var options = {
    hostname : '127.0.0.1',
    port     : 8080,
    path     : 'www.baidu.com:80',
    method     : 'CONNECT'
};

var req = http.request(options);

req.on('connect', function(res, socket) {
    socket.write('GET / HTTP/1.1\r\n' +
                 'Host: www.baidu.com:80\r\n' +
                 'Connection: Close\r\n' +
                 '\r\n');

    socket.on('data', function(chunk) {
        console.log(chunk.toString());
    });

    socket.on('end', function() {
        console.log('socket end.');
    });
});

req.end();

代理服务器代码:

const http = require('http');
const net = require('net');
const url = require('url');
let proxy = http.createServer();
proxy.on('connect' , function (req , cltSocket , head) {
    let u = url.parse('http://' + req.url);
    let pSock = net.connect(u.port , u.hostname , function () {
        cltSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
        pSock.pipe(cltSocket);
    });
    cltSocket.pipe(pSock);
});
proxy.listen(8080);
console.log('listening port 8080');

https隧道代理:

// https隧道代理服务器
const https = require('https');
const net = require('net');
const url = require('url');
const fs = require('fs');
let options = {
    key : fs.readFileSync('./private.pem'),
    cert : fs.readFileSync('./public.crt')
};
let proxy = https.createServer(options);
proxy.on('connect' , function (req , cltSocket , head) {
    let u = url.parse('http://' + req.url);
    let pSock = net.connect(u.port , u.hostname , function () {
        cltSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
        pSock.pipe(cltSocket);
    });
    cltSocket.pipe(pSock);
});
proxy.listen(8080);
console.log('listening port 8080');
// https,connect请求
var http = require('http');
var https = require('https');
var options = {
    hostname : '127.0.0.1',
    port     : 8080,
    path     : 'www.jobui.com:80',
    method     : 'CONNECT'
};
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
var req = https.request(options);
req.on('connect', function(res, socket) {
    socket.write('GET / HTTP/1.1\r\n' +
                 'Host: www.jobui.com:80\r\n' +
                 'Connection: Close\r\n' +
                 '\r\n');

    socket.on('data', function(chunk) {
        console.log(chunk.toString());
    });

    socket.on('end', function() {
        console.log('socket end.');
    });
});

req.end();

React组件代码分割

React组件代码拆分

当一个人在访问我们的web应用时,我们不需要让我们的用户在使用web应用时加载整个应用代码。我们可以认为代码分割就像是逐步加载应用程序,我们只加载我们需要的应用程序,不需要的可以不加载。要想完成这个功能,我们可以使用webpack,@babel/plugin-syntax-dynamic-import和react-loadable。我们这里主要讲一下react-loadable是怎么做到的?

react-loadable

Loadable是一个高阶组件,是一个小型库,它使得以组件为中心的代码在React中非常容易分割。

react-loadable可以在组件渲染到你的应用前动态的加载任何组件。

使用方式

Loadable({
    // 需要加载的组件
    loader : () => import('./myComponent'),   
    // loading组件
    loading : myLoadingComponent,  
    // 组件加载的时间大于delay的时候,则显示loading组件
    delay : 1000      
})

一般我们只需要前面两个选项就可以了。

代码分割

通过使用import()来进行组件代码分割。

demo:

// App.js
import React, { Component } from 'react';
import Loadable from 'react-loadable';
import Loading from './Loading';
const Container = Loadable({
    loader : () => import('./Hello'),
    loading : Loading
});


class App extends Component {
    constructor (props) {
        super(props);
    }
    render () {
        return (
            <Container />
        )
    }
}
// Hello.js
import React , { Component } from 'react';
class Hello extends Component {
    render () {
        return (
            <div>hello world</div>
        )
    }
};
export default Hello;
// Loading.js
import React , { Component } from 'react';
class Loading extends Component {
    constructor (props) {
        super(props);
    }
    render () {
        return <div>加载中...</div>;
    }
}
export default Loading;

上面的代码就是一个简单的react-loadable的应用。通过import()方法来动态的加载Hello组件,并且会自动的将Hello组件的代码拆分出来。

image

当我们刷新页面的时候,发现"加载中..."组件是一闪而过,主要是因为页面没有任何其他东西,所以加载会很快,那么这个时候,我们可以通过配置来解决,当组件加载时间超过200毫秒,那么就显示loading组件,如果小于200毫秒,那么就不显示loading组件。我们这里只需要改一下Loading组件就可以了。

// Loading.js
import React , { Component } from 'react';
class Loading extends Component {
    constructor (props) {
        super(props);
    }
    render () {
        const { error , pastDelay } = this.props;
        if (error) {
            return <div>Error</div>;
        } else if (pastDelay) {
            return <div>加载中...</div>;
        } else {
            return null;
        }
    }
}
export default Loading;
自定义渲染

react-loadable会渲染默认的组件,如果你想自定义组件,那么可以使用render选项,render选项是一个函数,接收两个参数,第一个参数是要加载的组件,第二个参数是传递给这个要加载的组件的props,该函数返回一个组件。比如:

// 这里是我定义的一个新的组件
class MyComponent extends Component {
    constructor (props) {
        super(props);
    }
    render () {
        return (
            <div style={{color : this.props.color}}>my name is andy</div>
        )
    }
}

// 这里是调用loadable返回要加载的组件
const Container = Loadable({
    loader : () => import('./Hello'),
    loading : Loading,
    // 这里使用render来自定义用户需要加载的组件,而不是加载Hello组件
    // loaded是一个对象,该对象有一个default属性,该属性表示的就是要加载的组件(Hello组件)
    // props属性,就是该组件接收到的父组件传递下来的props
    render (loaded , props) {
        return <MyComponent {...props} />
    }
});


class App extends Component {
    constructor (props) {
        super(props);
    }
    render () {
        return (
            <Container color="red" />
        )
    }
}
加载多个资源

当我们需要加载多个资源的时候,我们可以使用Loadable.Map来实现,其实和Loadable差不多,只是loader选项是一个对象,而不是一个函数。

const Container = Loadable.Map({
    loader : {
        Hello : () => import('./Hello'),
        weather : () => axios.get('https://free-api.heweather.com/s6/weather/now?location=广州&key=cb0ad81a470b48e2a21028ddb429d237').then(res => res.data.HeWeather6[0])
    },
    loading : Loading,
    render (loaded , props) {
        // loaded参数存放的就是已经加载的资源
        let Hello = loaded.Hello.default;
        let weather = loaded.weather;
        return <Hello {...props} weather={weather} />
    }
})
预加载

我们可以在展示组件之前,预先加载组件资源,比如:

const Container = Loadable({
    loader : () => import('./Hello'),
    loading : Loading
});

class App extends Component {
    constructor (props) {
        super(props);
        this.state = {
            show : false
        };
    }
    showContainer () {
        this.setState({
            show : true
        });
    }
    onMouseOver () {
        Container.preload();
    }
    render () {
        return (
            <div>
                <button onClick={() => this.showContainer()} onMouseOver={() => this.onMouseOver()}>show Container</button>
                {this.state.show ? <Container /> : null}
            </div>
        )
    }
};

当我们把鼠标放在按钮上是,就开始预先加载组件,等到点击按钮时,就把组件展示出来。

在服务器端预加载所有的组件

这里我们需要使用Loadable.preloadAll()方法来实现,调用该方法返回一个promise对象,当所有的组件都加载完成后,我们在监听端口号,比如:

import express from 'express';
import React , { Component } from 'react';
import ReactDOMServer from 'react-dom/server';
import Loadable from 'react-loadable';
import Loading from './components/Loading';
import delay from './utils/index';
const app = express();

console.time();

const AppLoadable = Loadable({
    // 我这里只是模拟了组件加载了3秒钟
    loader : () => delay(3000).then(() => import('./components/App')),
    loading : Loading
});

app.get('/' , (req , res) => {
    res.send(`
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <meta http-equiv="X-UA-Compatible" content="ie=edge">
            <title>Document</title>
        </head>
        <body>
            <div id="app">${ReactDOMServer.renderToString(<AppLoadable />)}</div>
        </body>
        </html>
    `)
});
// 在组件加载了3秒钟之后,服务器才监听3000端口
// 这样就达到了在服务器端预加载组件
Loadable.preloadAll().then(() => {
    console.timeEnd();
    app.listen(3000 , () => {
        console.log('listening port 3000');
    })
});
查找动态加载的组件

我们可以使用Loadable.Capture组件实现。<Loadable.Capture />组件接收一个report的属性,该属性是一个函数,该函数接受一个参数,这个参数就是已经加载的组件模块,我们就可以将这个组件模块添加到一个数组中,并且打印出来,这样我们就可以找出有多少个组件是动态加载的了。

import express from 'express';
import React , { Component } from 'react';
import ReactDOMServer from 'react-dom/server';
import Loadable from 'react-loadable';
import Loading from './components/Loading';
import delay from './utils/index';
const app = express();

app.use(express.static('dist'));

console.time();

const AppLoadable = Loadable({
    loader : () => import('./components/App'),
    loading : Loading
});

app.get('/' , (req , res) => {
    let modules = [];
    let html = `
                <!DOCTYPE html>
                <html lang="en">
                <head>
                    <meta charset="UTF-8">
                    <meta name="viewport" content="width=device-width, initial-scale=1.0">
                    <meta http-equiv="X-UA-Compatible" content="ie=edge">
                    <title>Document</title>
                </head>
                <body>
                    <div id="app">${ReactDOMServer.renderToString(
                        <Loadable.Capture report={moduleNames => modules.push(moduleNames)}>
                            <AppLoadable />
                        </Loadable.Capture>
                    )}</div>
                    <script src="./main.js"></script>
                </body>
                </html>
            `;
        console.log(modules);
    res.send(html);
});

Loadable.preloadAll().then(() => {
    console.timeEnd();
    app.listen(3000 , () => {
        console.log('listening port 3000');
    })
});

结果为:

[ './components/App' ]

长轮询和短轮询

长轮询和短轮询

短轮询

短轮询:指的是客户端发起一个请求,服务器无论是否有新数据,都立即返回(有就返回新数据,没有就返回一个表示数据为空的响应),然后http连接断开。

setInterval(function () {
    $.ajax('/user' , funciton (data) {
        console.log(data);
    })
} , 1000)

上面的代码就是每隔1秒钟向服务器发送一次请求,服务器返回数据,客户端再对数据进行处理。这里有一个问题需要注意,如果当前网络比较慢时,服务器从接受请求,返回数据到客户端接受请求的总时间有可能会超过1秒,而请求是每隔1秒发送一次,这样会导致接收的数据到达先后顺序与发送顺序不一致。于是我们可以采用setTimeout的方式来进行轮询,来避免这样的情况发生。

function poll () {
    setTimeout(function () {
        $.ajax('/user' , function (data) {
            console.log(data);
            poll();
        })
    } , 1000)
}
poll();

长轮询

长轮询:指的是由客户端发起请求,如果服务器没有相关数据,那么服务器会hold住请求,直到服务器有相关数据,或等待一定时间超时才会返回。返回后,客户端又会发起请求。这个是需要客户端和服务器两者共同来完成的,我们来简单实现一个:

客户端代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<h1>hello world</h1>
<button id="btn">获取用户信息</button>
<script>
    var btn = document.getElementById('btn');
    btn.addEventListener('click' , (e) => {
        sendXhr();
    });
    function sendXhr () {
        var xhr = new XMLHttpRequest();
        xhr.open('get' , '/user');
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4) {
                if (xhr.status == 200) {
                    var result = JSON.parse(xhr.responseText);
                    // 如果服务器没有数据,那么会再一次发起请求
                    if (!result['result']) {
                        sendXhr();
                    } else {
                        console.log('有数据了');
                        console.log(result);
                    }
                } else {
                    console.log('失败');
                }
            }
        };
        xhr.send(null);
    }
</script>
</body>
</html>

服务端代码:

const express = require('express');
const app = express();
app.set('view engine' , 'html');
app.engine('html' , require('ejs').renderFile);
app.set('views' , './views');
app.use('/public' , express.static('static'));

app.get('/' , (req , res) => {
    res.render('./index.html');
});
app.get('/user' , (req , res) => {
    function sleep(count) {
        function inner () {
            let timer = setTimeout(function () {
                if (count == 1) {
                    clearTimeout(timer);
                    return res.status(200).json({result : ''});
                } else {
                    count--;
                    inner();
                }
            } , 1000)
        };
        inner();
    };
    sleep(5);
});
app.listen(3000 , () => {
    console.log('listening port on 3000');
})

就简单实现了一下,通过上面代码可知,当在客户端点击按钮时,会向服务器发送请求,而服务器上的代码,则会hold住请求5秒,然后再将数据发送给客户端,当客户端判断数据为空的时候,又重新向服务器发送请求。长轮询就是这样的一个过程。

心跳机制

心跳机制:指的是客户端每隔N秒向服务器发送一个心跳消息,服务器收到客户端的心跳小心后,回复同样的心跳消息给客户端,如果服务器或客户端再M秒(M>N)内没有收到心跳消息在内的任何消息,即心跳超时,那么我们就认为该连接已经断开了。

如何创建对象

如何创建对象

在javascript中,创建对象的方式有很多种,<javascript高级程序设计>中有很详细的介绍,具体来看一下:

1、工厂模式

工厂模式,是将创建对象的具体过程封装在一个函数中,当需要重复创建对象的时候,只需要调用这个函数就能创建多个对象。

function createObjectFactory (name , age) {
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.sayName = function () {
        console.log(this.name);
    };
    return obj;
};

优点:将创建对象的具体细节封装起来,需要创建多个相似对象时,只需要调用这个工厂函数即可。

缺点:对象不能识别,因为创建出来的对象都指向一个原型。

2、构造函数模式

通过new操作符来创建对象

function Person (name , age) {
    this.name = name;
    this.age = age;
    this.sayName = function () {
        console.log(this.name);
    }
};
var andy = new Person('andy' , 22);
var jack = new Person('jack' , 23);

优点:创建的对象可以识别为一种特定类型。

缺点:每个方法都要在每个实例上重新创建一遍。

2.1、改造构造函数模式

function Person (name , age) {
    this.name = name;
    this.age = age;
    this.sayName = sayName;
};

function sayName () {
    console.log(this.name);
}

上面的代码中,我们将对象的方法,指向全局作用域中的一个函数,这样在多次创建对象的时候,不需要重新创建一遍方法,对象的方法都指向同一个变量。

缺点:定义在全局作用域中的函数,只能被某个对象调用,这完全没有什么封装性。

3、原型模式

function Person () {

}
Person.prototype.name = 'andy';
Person.prototype.age = 22;
Person.prototype.sayName = function () {
    console.log(this.name);
}

优点:方法不会被重新创建。

缺点:所有创建的实例共享属性和方法,并且不能初始化参数。

3.1、改造原型模式

function Person () {

}
Person.prototype = {
   name : 'andy',
   age : 22,
   sayName : function () {
       console.log(this.name);
   } 
}

缺点:重写了构造函数的原型,构造函数的constructor属性丢失。

优点:将属性和方法封装在一起。

3.2、改造原型模式

function Person () {

}
Person.prototype = {
    constructor : Person,
    name : 'andy',
    age : 22,
    sayName : function () {
        console.log(this.name);
    } 
}

优点:实例可以通过constructor属性找到构造函数。

缺点:所有实例共享属性和方法。

4、组合模式

将构造函数模式和原型模式组合一起使用。

function Person (name , age) {
    this.name = name;
    this.age = age;
}

Person.prototype = {
    constructor : Person,
    sayName : function () {
        console.log(this.name);
    } 
}

优点:该共享的共享,该私有的私有。

4.1、动态原型模式

我们可以看到原型模式中,构造函数和原型是分开的,如果我们想要把它们封装在一起,那么我们可以使用动态原型模式,我们去判断原型中如果没有这个方法,我们动态的在原型上添加这个方法。

function Person (name , age) {
    this.name = name;
    this.age = age;
    if (typeof this.sayName != 'function') {
        Person.prototype.sayName = function () {
            console.log(this.name);
        }
    };
}
// 重写原型
function Person (name , age) {
    this.name = name;
    this.age = age;
    if (typeof this.sayName != 'function') {
        Person.prototype = {
            constructor : Person,
            sayName : function () {
                console.log(this.name);
            }
        }
    };
};
var andy = new Person('andy' , 22);
andy.sayName();    // 这里会报错,没有这个方法

注意:在使用动态原型模式的时候,不能使用对象字面量重写原型。这里我们需要了解通过调用构造函数来创建对象的过程:

  • 创建一个新的空对象。
  • 将对象的原型指向构造函数的prototype属性。
  • 调用构造函数,并将this绑定到新创建的对象。
  • 返回新对象。

所以在使用构造函数来创建对象时,通过上面步骤,我们发现,当执行构造函数的时候,其实已经执行了前面两个步骤,即:创建了一个新的空对象,并且已经将对象的原型指向构造函数的prototype属性,所以执行if条件判断的时候,如果没有sayName方法,那么将Person.prototype属性直接通过对象字面量进行了覆盖,并不会更改对象原型的值,对象的原型还是指向之前的那个Person.prototype属性,而不是被覆盖后的。由于之前原型上根本就没有sayName的方法,所以就会报错。这里说的有点绕,举个例子:

var a = {
    name : 'andy'
};
var b = {
    name : 'jack'
};
a = b;
b = {
    name : 'peter'
}
console.log(a);   // 结果是:{name : 'jack'}

5、寄生构造函数模式

其实就是通过寄生在构造函数模式上的一种方式来创建对象。

function Person (name , age) {
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.sayName = function () {
        console.log(this.name);
    }
    return obj;
}

var person = new Person('andy' , 22);
person.sayName();

上面的代码,我们会发现,其实寄生构造函数模式中,内部是自己创建一个对象,然后返回这个对象,我们知道当使用new构造函数的方式来创建对象时,如果构造函数中有返回值,并且该返回值是一个对象,那么就会直接返回这个对象,而这个对象的类型不是Person而是Object,而且也不会继承构造函数的原型。

这个模式可以用来为对象创建构造函数,比如,我们想要创建一个拥有额外方法的特殊数组,由于不能直接在Array.prototype上添加,所以我们可以使用寄生构造函数模式来创建这样一个数组:

function SpecialArray () {
    var array = new Array();
    array.push.apply(array , arguments);
    array.toSpipedString = function () {
        return array.join('|');
    }
    return array;
}
var aa = new SpecialArray('andy' , 'jack' , 'peter');
console.log(aa.toSpipedString())

6、稳妥构造函数模式

稳妥构造函数模式与寄生构造函数模式类似,但是有两点不同,第一就是新创建对象的实例方法不引用this,第二就是不使用new来调用构造函数。

function Person (name) {
    var obj = new Object();
    obj.sayName = function () {
        console.log(name);
    }
    return obj;
}

var person = Person('andy');
person.sayName();

BrowserRouter组件

react路由组件之BrowserRouter组件

react路由组件使用的是HMTL5的history对象的方法来实现页面的UI渲染和地址保持同步。

BrowserRouter组件的属性

  • basename属性
    • 该属性是一个字符串,指的是为react应用指定一个根路径。
  • getUserConfirmation属性
    • 该属性是一个函数,表示当导航到该页面前会执行的操作,一般当需要用户进入页面前执行什么操作时可用。
  • forceRefresh属性
    • 该属性是一个布尔值,当为true的时候,强制刷新浏览器,一般用于当浏览器不支持HMTL5的historyAPI的时候,强制刷新页面。
  • keyLength属性
    • 该属性是一个数字,表示location.key的长度。默认location.key是6位数。点击同一个链接时,每次该路由下的location.key都会刷新。

BrowserRouter只能有一个子节点。

// 当我们设置了basename,那么链接的path匹配的就是"/home/one"
class App extends Component {
    render () {
        return (
            <Router
                basename="/home"
            >
                <div>
                    <Link to="/one">one</Link>
                    <Route path="/one" render={() => (
                        <h1>one</h1>
                    )} />
                    <Route path="/" exact render={() => (
                        <h1>index</h1>
                    )} />
                </div>
            </Router>
        )
    }
}

当我们设置了basename,那么链接的path匹配的就是"/home/one"

class App extends Component {
    confirmation = () => {
        window.confirm('are you sure?');
    }
    render () {
        return (
            <Router
                basename="/home"
                // 一般用于当浏览器不支持HMTL5的historyAPI的时候,强制刷新页面
                forceRefresh={!('pushState' in window.history)}
                getUserConfirmation={this.confirmation}
            >
                <div>
                    <Link to="/one">one</Link>
                    <Route path="/one" render={() => (
                        <h1>one</h1>
                    )} />
                </div>
            </Router>
        )
    }
}

上面代码,我们可以通过判断window.history对象中是否存在pushState方法来判断是否支持HTML5的history API。如果不支持的话,我们就强制刷新页面。

class App extends Component {
    confirmation = () => {
        window.confirm('are you sure?');
    }
    render () {
        return (
            <Router
                basename="/home"
                // 一般用于当浏览器不支持HMTL5的historyAPI的时候,强制刷新页面
                forceRefresh={!('pushState' in window.history)}
                getUserConfirmation={this.confirmation}
                keyLength={10}
            >
                <div>
                    <Link to="/one">one</Link>
                    <Route path="/one" render={({location}) => {
                        console.log(location.key)
                        return (
                            <h1>one</h1>
                        )
                    }} />
                </div>
            </Router>
        )
    }
}

上面代码,我们设置location.key的长度为10。每次点击同一个链接,每次的location.key都是不一样的。

class App extends Component {
    confirmation = (message , callback) => {
        let flag = window.confirm(message);
        callback(flag);
    }
    render () {
        return (
            <Router
                basename="/home"
                // 一般用于当浏览器不支持HMTL5的historyAPI的时候,强制刷新页面
                forceRefresh={!('pushState' in window.history)}
                // 当导航到该页面的时候,会先调用该函数
                getUserConfirmation={this.confirmation("are you sure?" , (flag) => {
                    console.log(flag);
                })}
                keyLength={10}
            >
                <div>
                    
                </div>
            </Router>
        )
    }
}

上面代码,当用户进入该页面的时候,会先执行confirmation函数。主要是用在页面进入前可能用户会执行一些操作的情况。

总结

1、BrowserRouter组件只能有一个子节点,一般react应用的代码都会被BrowserRouter组件包裹。类似这样。

2、BrowserRouter组件有三个属性:history,location,match。可以在和组件中通过props获取到,history对象主要用于路由导航操作,location对象包含了当前的一些路径信息(比如:pathname(路径),search(查询字符串),key,hash等),match对象包含了路径匹配的参数信息。

浏览器渲染过程

浏览器渲染过程

  • 1、解析HTML,构建DOM树
  • 2、解析CSS,构建CSSOM树
  • 3、将DOM树和CSSOM树合并成一个渲染树
  • 4、根据渲染树来进行布局,计算每个节点的几何信息
  • 5、将各个节点绘制到屏幕上

为了构建渲染树,浏览器主要完成以下几个工作:

  • 1、从DOM树的根节点开始遍历每个可见节点。
    • 某些节点不可见(比如script,meta标签等),因为它们不会体现在渲染输出中,所以会忽略。
    • 某些节点通过CSS隐藏,因此在渲染树中也会被忽略,比如一个节点设置了“dispaly:none”属性
  • 2、对于每个可见节点,为其找到适配的CSSOM规则并应用它们。
  • 3、根据每个可见节点以及它们对应的样式,组合生成渲染树。

注意的是:渲染树只包含可见节点

回流

浏览器将DOM树和CSSOM树组合成渲染树之后,还需要计算节点在设备视口内的确切位置和大小,这个计算的阶段就是回流。

为弄清每个节点在网页上的确切大小和位置,浏览器从渲染树的根节点开始进行遍历。我们来看一下例子:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>

我们可以看到第一个div将节点的显示尺寸设置为视口宽度的50%,父div包含的第二个div将其宽度设置为其父节点宽度的50%,即视口的25%。而在回流阶段,我们就需要根据视口宽度,将其转为具体的像素值。

image

重绘

既然我们知道了哪些可见节点,它们的计算样式以及几何信息,我们终于可以将这些信息传递给最后一个阶段,将渲染树上的每个节点转换成屏幕上的实际像素。这一步通常被称为:重绘。

什么时候会发送回流重绘

我们了解到,回流阶段主要是计算节点的位置或几何信息,那么当页面布局和几何信息发生变化的时候,就需要进行回流。比如:

  • 1、添加或删除可见DOM元素
  • 2、DOM元素的位置发生变化
  • 3、DOM元素的尺寸大小发生变化
  • 4、DOM元素的内容发生变化
  • 5、浏览器的窗口尺寸发生变化

这里需要注意的是:回流一定会导致重绘,但是重绘不一定会导致回流

当我们获取DOM节点的布局信息时,也会导致浏览器回流重绘。比如:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight、width、height等

可以访问这里去查看:

当我们获取节点的这些属性值时,都会导致浏览器回流重绘,因为当我们获取这些值的时候,需要返回最新的值,所以浏览器会触发回流,重新计算布局信息,来返回正确的值。

减少回流和重绘

1、合并多次对DOM样式的修改

let box = document.getElementById('box');
box.style.padding = '10px';
box.style.borderTop = '20px';
box.style.width = '100px';

向上面的例子中,每次重新设置节点的样式都会导致浏览器回流重绘,所以最好是将修改样式的代码合并成一句,比如:

box.style.cssText = 'padding:10px;borderTop : 20px;width:100px';

或者也可以通过添加一个样式类,来达到这样的效果。

批量修改DOM

当我们需要对DOM进行一系列修改的时候,我们可以这样来减少回流重绘次数:使元素脱离文档流,然后对其进行多次修改,最后将元素放回到文档中。

这里有三种方式可以使DOM脱离文档流:

  • 隐藏元素,然后对元素修改,最后再重新显示
  • 使用文档片段(document Fragment)
  • 将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素

如果我们需要通过循环,批量插入节点到文档中,一般的做法都是:

let list = document.getElementById('list');
let data = [1,2,3,4,5,6,7,8,9,10];

let appendToList = function (target , data) {
    for (let i = 0 ; i < data.length ; i++) {
        let li = document.createElement('li');
        li.innerHTML = data[i];
        target.appendChild(li);
    }
};

appendToList(list , data);

但是每一次插入节点的时候都会引起浏览器回流重绘,所以我们可以使用这三种方式来进行优化:

隐藏元素
let list = document.getElementById('list');
list.style.display = 'none';
let data = [1,2,3,4,5,6,7,8,9,10];

let appendToList = function (target , data) {
    for (let i = 0 ; i < data.length ; i++) {
        let li = document.createElement('li');
        li.innerHTML = data[i];
        target.appendChild(li);
    }
};

appendToList(list , data);
list.style.display = 'block';
使用文档片段
let list = document.getElementById('list');
let data = [1,2,3,4,5,6,7,8,9,10];

let appendToList = function (target , data) {
    for (let i = 0 ; i < data.length ; i++) {
        let li = document.createElement('li');
        li.innerHTML = data[i];
        target.appendChild(li);
    }
};
let fragment = document.createDocumentFragment();
appendToList(fragment , data);
list.appendChild(fragment);
将原始元素拷贝到一个脱离文档的节点中
let list = document.getElementById('list');
let data = [1,2,3,4,5,6,7,8,9,10];

let appendToList = function (target , data) {
    for (let i = 0 ; i < data.length ; i++) {
        let li = document.createElement('li');
        li.innerHTML = data[i];
        target.appendChild(li);
    }
};
let clone = list.cloneNode(true);
appendToList(clone , data);
list.parentNode.replaceChild(clone , list);

对于一些复杂的动画,可以使用绝对定位使其脱离文档流

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
    
    #box {
      background: red;
      animation: scale 4s linear 0s infinite alternate;
      background-image: linear-gradient(to right, rgba( 0,0,0,0.9 ) 25%, rgba( 0,0,0,0.1 ) 50%, rgba( 0,0,0,0.9 ) 75%);
      will-change: all;
      transform: translate3d(0, 0, 0);
    }

    @keyframes scale {
      from { 
        width: 100px; 
        height: 100px;
        background: red;
        margin: 10px;
        transform: rotate(0);
        margin-left: -20%;
        rotate: 10deg;
      }
      to {
        width: 200px;
        height: 200px;
        background: yellow;
        margin: 50px;
        transform: rotate(360deg);
        margin-left: 100%;
      }
    }
    </style>
</head>
<body>
    <div id="box"></div>
    <button id="btn">click</button>
    <p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p>
    <p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p>
    <p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p>
    <p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p>
    <p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p>
    <p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p>
<script>

let box = document.getElementById('box');
btn.addEventListener('click' , function () {
    box.style.position = 'absolute';
});

let frame = 0;
let start = null;
function loop (timestamp) {
    if (!start) {
        start = timestamp;
    }
    frame++;
    let diff = timestamp - start;
    if (diff > 1000) {
        console.log('1秒内刷新频率为:' + Math.round((frame * 1000) / diff));
    }
    requestAnimationFrame(loop);
}
requestAnimationFrame(loop)
</script>
</body>
</html>

事件

事件

在前端开发中,我们不可避免的会与各种事件打交道,对于javascript中的事件,我们有几个部分是一定需要了解清楚的。

  • 事件绑定的方式有哪些
  • 事件模型
  • 事件委托
  • 事件对象

事件绑定的方式

我们这里以点击事件为例

  • 第一种:

  • 第二种:DOM.onclick = function () {}

  • 第三种:DOM.addEventListener('click' , function () {} , false/true)

三种绑定方式的区别:

  • 第一种方式是直接内嵌在html代码中,将js和html耦合在一起,这种方式是不推荐的。
  • 第二种方式是无法同时触发多个事件回调函数,当同时赋值多个事件回调函数时,后一个都会覆盖前一个事件回调函数,所以当事件触发时,最终执行的就是最后一个事件回调函数,前面的都不会执行
  • 第三种方式可以触发多个事件回调函数,第三种方式其实就是将多个事件回调函数一个一个的添加到任务队列中,等到事件被触发的时候,就依次以添加时的顺序一个一个的执行,直到所有的回调函数都执行完

事件模型

事件,我们可以把它分为三个阶段:捕获阶段,目标阶段,冒泡阶段。

捕获阶段:指的就是从最外层容器向目标对象一层一层的传递,也就是从外向内传递。

目标阶段:指的就是从目标事件对象的父节点到目标事件对象这一阶段。

从目标事件对象开始从下往上传递一直到根元素为止,这一阶段被称为事件冒泡阶段。

事件对象的几个常用的属性和方法

  • e.target:事件目标对象
  • e.preventDefault():阻止事件默认行为(比如a标签的默认行为就是跳转到指定的链接上)
  • e.stopPropagation():阻止事件冒泡

target,currentTarget,this的区别

  • event.target表示的是事件目标节点,你点击哪个元素,target表示的就是哪个元素,可以说这个表示的是事件的真正触发者。
  • event.currentTarget表示的是事件的监听者。
  • this表示的是当前事件对象。

事件委托

事件委托就是利用事件冒泡机制,指定一个事件处理程序,来管理某一类型的所有事件。

为什么要用事件委托?

  • 使用事件委托,我们就能够避免给每一个DOM元素绑定同一类型的事件回调函数,可以减少内存的占用。
  • 使用事件委托,还可以不用给新插入进来的DOM元素绑定事件回调函数。

react高阶组件

react高阶组件

高阶组件指的是:接受一个组件作为参数,返回一个新组件的函数。

function HOC (WrappedComponent) {
    return class NewComponent extends Component {
        constructor (props) {
            super(props);
        }
        render () {
            return (
                <WrappedComponent {...this.props} />
            )
        }
    }
}

为什么要用高阶组件?

我觉得主要还是为了代码复用。比如说,有一个需求,我需要获取短评列表数据和长评列表数据并展示在页面上,其实获取数据的逻辑都是一样的,只是获取的接口不一样,正常来说,我们会短评组件上编写处理逻辑,然后再长评组件上编写长评处理逻辑,这样你会发现其实逻辑是一样的,就因为接口不一样,而同样的逻辑写了两遍是不是有点太多余了呢,这个时候我们可以使用高阶组件,来进行代码复用。

const url1 = 'http://localhost:8000/api/shortcomment/short';
const url2 = 'http://localhost:8000/api/longcomment/long';
// 高阶组件
function HOC (WrappedComponent , url) { // 传入一个参数
    return class NewComponent extends WrappedComponent {
        constructor (props) {
            super(props);
            this.state = {
                data : []
            }
        }
        componentDidMount () {
            // 获取数据
            axios.get(`${url}`).then(res => {
                this.setState({
                    data : res.data.commentList
                })
            })
        }
        render () {
            const {data} = this.state;
            return (
                <WrappedComponent {...this.props} data={data} />
            )
        }
    }
};

// 短评组件
class ShortComponent extends Component {
    constructor (props) {
        super(props);
    }
    render () {
        const { data } = this.props;
        return (
            <div>
                <h1>这是短评列表</h1>
                <div>
                    {
                        data.map(item => (
                            <div key={item.id}>{item.avatar}</div>
                        ))
                    }
                </div>
            </div>
        )
    }
};
// 长评组件
class LongComponent extends Component {
    constructor (props) {
        super(props);
    }
    render () {
        const { data } = this.props;
        return (
            <div>
                <h1>这是长评列表</h1>
                <div>
                    {
                        data.map(item => (
                            <div key={item.id}>{item.avatar}</div>
                        ))
                    }
                </div>
            </div>
        )
    }
}

const ShortCommentComponent = HOC(ShortComponent , url1);
const LongCommentComponent = HOC(LongComponent , url2);

class App extends Component {
  render() {
    return (
        <div>
            <ShortCommentComponent />
            <LongCommentComponent />
        </div>
    );
  }
}

高阶组件的两种主要形式

react中高阶组件主要有两种形式:属性代理和反向继承。

属性代理

属性代理,其实就是一个函数接受一个包装组件作为参数,返回一个新组件,新组件的render方法中返回传入的包装组件,并将需要处理的props和新的props传入到新组件中。

function HOC (WrappedComponent) {
    return class NewComponent extends Component {
        constructor (props) {
            super(props);
            this.state = {name : 'andy'}
        }
        render () {
            const newProps = this.state;
            return (
                <WrappedComponent {...this.props} {...newProps} />
            )
        }
    } 
}

class OriginComponent extends Component {
    render () {
        const {name} = this.props;
        return (
            <div>{name}</div>
        )
    }
}

const AComponent = HOC(OriginComponent);

反向继承

高阶函数接受一个组件作为参数,然后在函数里面返回一个新组件,并且新组件继承传入的组件。

Vuex基础

Vuex

Vuex是一个专门为Vuejs应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

State

Vuex使用单一状态树,用一个对象就包含了全部的应用状态层级,它就是一个唯一的数据源,这就意味着每个应用仅仅只能包含一个store实例。我们来看一下是如何定义state的。

const store = new Vuex.Store({
    state : {
        count : 1,
        name : 'andy',
        age : 12
    }
});

上面的代码中,我们就已经定义了一个state了,那么我们在组件中怎么获取到这个state呢?我们可以在组件的计算属性上返回我们需要的状态,比如:

export default {
    data () {
        return {
            name : 'andy'
        }
    },
    computed : {
        count () {
            return this.$store.state.count;
        },
        name () {
            return this.$store.state.name;
        },
        age () {
            return this.$store.state.age;
        }
    }
}

通过上面的方式,我们可以在计算属性上返回我们当前组件需要使用的状态。但是这样的方式,让代码看起来会有点冗余和重复,这时候我们可以使用mapState函数来帮助我们生成计算属性。

const store = new Vuex.Store({
    state : {
        count : 1,
        name : 'andy',
        age : 12
    }
});
import { mapState } from 'vuex';
export default {
    data () {
        return {
            a : 1
        }
    },
    computed : mapState(['count' , 'name' , 'age'])
}

上面的代码中,我们可以看到,如果组件的计算属性的名称与状态名称一样的话,我们可以在调用mapState函数时传入一个数组,数组的元素就是组件的计算属性的名称,而计算属性的值就是这些名称对应的state的值。

如果计算属性的名称与state的名称不一样呢?那么我们可以是这样的:

export default {
    data () {
        return {
            a : 1
        }
    },
    computed : mapState({
        num : state => state.count,
        who : "name"  // 类似于:who : state => state.name
    })
}

上面代码中,调用mapState函数,传入的是一个对象,对象的key就是计算属性的名称,key对应的value可以是一个函数或者字符串,如果是函数的话,那么函数接受一个state作为参数,返回对应的状态值。

如何将state和组件内的局部计算属性混合在一起使用呢?mapState函数返回的是一个对象,我们可以使用对象的扩展运算符,比如:

export default {
    data () {
        return {
            a : 1
        }
    },
    computed : {
        address () {
            return '广州';
        },
        ...mapState({
            num : state => state.count,
            who : 'name'
        })
    }
}

Getter

有时候我们需要从store中的state中派生出一些状态,比如对列表进行过滤等。Vuex允许我们在store中定义"getters",这个我们可以理解为store的计算属性,就像计算属性一样,getter的返回值会根据它的依赖被缓存起来,只有当它的依赖值发生改变才会重新计算。getter接受state作为第一个参数。

export default {
    data () {
        return {
            a : 1
        }
    },
    computed : {
        completedTodos () {
            return this.$store.getters.completedTodos;
        }
    }
}
const store = new Vuex.Store({
    state : {
        todos : [
            {id : 1 , text : 'andy' , completed : true},
            {id : 2 , text : 'alex' , completed : false},
            {id : 3 , text : 'peter' , completed : true},
        ]
    },
    getters : {
        completedTodos (state) {
            return state.todos.filter(todo => {
                return todo.completed;
            })
        }
    }
});

上面代码中,我们可以在组件中通过$store.getters属性来访问我们需要的值。

我们也可以通过方法来访问,我们可以让getter返回一个函数,来实现给getter传入参数。比如:

const store = new Vuex.Store({
    state : {
        todos : [
            {id : 1 , text : 'andy' , completed : true},
            {id : 2 , text : 'alex' , completed : false},
            {id : 3 , text : 'peter' , completed : true},
        ]
    },
    getters : {
        completedTodos (state) {
            return state.todos.filter(todo => {
                return todo.completed;
            })
        },
        getTodoById (state) {
            return (id) => {
                return state.todos.find(todo => todo.id == id);
            }
        }
    }
});
export default {
    data () {
        return {
            a : 1
        }
    },
    computed : {
        completedTodos () {
            return this.$store.getters.completedTodos;
        },
        currentTodoText () {
            return this.$store.getters.getTodoById(2).text;
        }
    }
}

这里我们需要注意的是,getter在通过方法访问时,每次都会进行调用,而不会缓存结果。

除此之外,我们还可以使用mapGetters函数将store中的getter映射到局部计算属性上。

export default {
    data () {
        return {
            a : 1
        }
    },
    computed : {
        ...mapGetters(['completedTodos'])
    }
}

export default {
    data () {
        return {
            a : 1
        }
    },
    computed : mapGetters({
        todos : 'completedTodos'  // 我们也可以使用不同的计算属性名
    })
}

Mutation

更改Vuex的store中的状态的唯一方法就是提交mutation。Vuex中的mutation非常类似于事件,每个mutation有一个字符串的事件类型(type)和一个回调函数(handle)。回调函数就是我们实际进行状态更改的地方,并且它会接受state作为第一个参数。

const store = new Vuex.Store({
    state : {
        todos : [
            {id : 1 , text : 'andy' , completed : true},
            {id : 2 , text : 'alex' , completed : false},
            {id : 3 , text : 'peter' , completed : true},
        ]
    },
    mutations : {
        addTodo (state , payload) {
            state.todos.push(payload);
        }
    }
});
import { mapState } from 'vuex';
export default {
    data () {
        return {
            a : 1
        }
    },
    computed : mapState(['todos']),
    methods : {
        addTodo () {
            this.$store.commit('addTodo' , {
                id : new Date().getTime(),
                text : Math.random().toFixed(2)
            })
        }
    }
}

这里我们需要注意的是,mutation必须是一个同步函数,如果mutation中有异步操作(比如:请求数据),那么当mutation触发的时候,异步数据都还没有返回。在Vuex中,mutation都是同步事务。

Action

action和mutation类似,不同在于,action提交的是mutation,而不是直接更改状态,action可以包含异步操作。

action函数接受一个与store实例具有相同方法行业属性的context对象,因此我们可以调用commit方法来提交一个mutation,或者通过context来获取state,getters等。

const store = new Vuex.Store({
    state : {
        count : 1
    },
    mutations : {
        increment (state) {
            state.count++;
        }
    },
    actions : {
        increment (context) {
            context.commit('increment');
        }
    }
});
import { mapState } from 'vuex';
export default {
    data () {
        return {
            a : 1
        }
    },
    methods : {
        increment () {
            this.$store.dispatch('increment');
        }
    },
    computed : mapState(['count'])
}

上面的代码感觉使用action有点多余,我直接commit一个mutation就可以,但是我们知道mutation必须是同步事务,而action是没有这个限制的,它里面可以包含异步事务。

const store = new Vuex.Store({
    state : {
        count : 1
    },
    mutations : {
        increment (state) {
            state.count++;
        }
    },
    actions : {
        increment (context) {
            context.commit('increment');
        },
        asyncIncrement (context) {
            setTimeout(() => {
                context.commit('increment');
            } , 2000)
        }
    }
});
import { mapState } from 'vuex';

export default {
    data () {
        return {
            a : 1
        }
    },
    methods : {
        increment () {
            this.$store.dispatch('increment');
        },
        asyncIncrement () {
            this.$store.dispatch('asyncIncrement');
        }
    },
    computed : mapState(['count'])
}

我们可以把异步封装成一个promise,那么我们可以在执行结束之后继续执行下面的逻辑:

asyncIncrementBy (context) {
    return new Promise((resolve , reject) => {
        setTimeout(() => {
            context.commit('increment');
            resolve();
        } , 2000)
    })
}

methods : {
    ...mapActions(['increment']),
    asyncIncrementBy () {
        this.$store.dispatch('asyncIncrementBy').then(res => {
            console.log(res)
            console.log(2324)
        })
    }
}

或者我们也可以使用async/await来进行异步流程控制。

Module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象,这样会显得store对象非常的臃肿。Vuex允许我们将store分割成模块,每个模块都有自己的state,mutation,action,getter。

Link组件

react路由组件之Link组件

在react应用中,react路由组件可以提供可声明的,可访问的导航组件。这个组件就是组件,组件最终会被渲染成a标签。

Link组件有以下几个属性

  • to属性
    • 该属性是一个字符串或者是一个对象,如果是一个字符串,那么表示要链接到的地址,可以通过location对象的pathname,search,hash这三个属性来创建,比如:About。如果to属性是一个对象,那么我们可以使用四个属性来构造链接(pathname:表示要链接的路径 , search:表示路径的查询参数 , hash:表示url的hash值 , state:表示保存在location的状态,我们可以设置一些值,这些值可以在location对象的state属性中获取)
  • replace属性
    • 该属性是一个布尔值,当为true时,点击链接时,会替换当前历史访问记录中的地址,而不是添加一个新的地址到历史访问记录中。
  • innerRef属性
    • 该属性是一个函数,当组件加载的时候,会调用这个函数,接收一个参数,是一个底层component引用。也就是说可以访问dom。

除了上面这些属性,我们也可以添加别的属性,id,className,title等,就像给a标签添加属性一样。

import React, { Component } from 'react';
import { BrowserRouter as Router , Route , Switch , Link , withRouter , Redirect } from 'react-router-dom';

const Home = () => <h1>home</h1>;
const About = (props) => {
    // 当导航到该页面的时候,我们可以打印看一下location对象下的属性
    console.log(props);
    return (
        <h1>about</h1>
    )
};
class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Link to="/">home</Link>
                    <Link to="/about?name=andy#abc">About</Link>
                    <Route path="/" exact component={Home} />
                    <Route path="/about" component={About} />
                </div>
            </Router>
        )
    }
}
export default App;

结果为:

image

import React, { Component } from 'react';
import { BrowserRouter as Router , Route , Switch , Link , withRouter , Redirect } from 'react-router-dom';

const Home = () => <h1>home</h1>;
const About = (props) => {
    console.log(props);
    return (
        <h1>about</h1>
    )
};
class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Link to="/">home</Link>
                    <Link to={{
                        pathname : '/about',
                        search : '?name=andy&age=22',
                        hash : '#abc',
                        state : {
                            name : 'andy',
                            job : 'doctor'
                        }
                    }}>About</Link>
                    <Route path="/" exact component={Home} />
                    <Route path="/about" component={About} />
                </div>
            </Router>
        )
    }
}
export default App;

结果为:

image

import React, { Component } from 'react';
import { BrowserRouter as Router , Route , Switch , Link , withRouter , Redirect } from 'react-router-dom';

const Home = () => <h1>home</h1>;
const About = (props) => {
    console.log(props);
    return (
        <h1>about</h1>
    )
};
const User = () => <h1>user</h1>;
const Login = () => <h1>login</h1>;

const AddressBtn = withRouter(class AddressBtn extends Component {
    constructor (props) {
        super(props);
    }
    prevPage = () => {
        let { history } = this.props;
        history.goBack();
    }
    nextPage = () => {
        let { history } = this.props;
        history.goForward();
    }
    render () {
        return (
            <div>
                <button onClick={this.prevPage}>上一页</button>
                <button onClick={this.nextPage}>下一页</button>
            </div>
        )
    }
})

// 当我们设置replace属性为true时,会替换当前历史访问记录中的地址,而不是添加一个新的地址到历史访问记录中
class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <AddressBtn />
                    <Link to="/">home</Link>
                    <Link to="/about" replace>about</Link>
                    <Link to="/user">user</Link>
                    <Link to="/login">login</Link>
                    <Route path="/" exact component={Home} />
                    <Route path="/about" component={About} />
                    <Route path="/user" component={User} />
                    <Route path="/login" component={Login} />
                </div>
            </Router>
        )
    }
}
export default App;

当我们在Link组件中设置replace属性为true时,点击该链接,会替换当前路由地址,而不是在历史访问记录里添加新的一个地址。当我们依次点击login,user,about按钮时,然后再点击上一页按钮,我们发现链接不会跳转到user,说明这里的user地址被about地址给替换了。

import React, { Component } from 'react';
import { BrowserRouter as Router , Route , Switch , Link , withRouter , Redirect } from 'react-router-dom';

const Home = () => <h1>home</h1>;

class About extends Component {
    constructor (props) {
        super(props);
        this.nodeRef = React.createRef();
    }
    render () {
        return (
            <h1 ref={this.nodeRef}>about</h1>
        )
    }
}
const User = () => <h1>user</h1>;
const Login = () => <h1>login</h1>;

class App extends Component {
    lookInnerRef = (node) => {
        console.log(node.innerHTML);
    }
    render () {
        return (
            <Router>
                <div>
                    <Link to="/">home</Link>
                    <Link to="/about" replace>about</Link>
                    <Link to="/user" innerRef={this.lookInnerRef}>user</Link>
                    <Link to="/login">login</Link>
                    <Route path="/" exact component={Home} />
                    <Route path="/about" component={About} />
                    <Route path="/user" component={User} />
                    <Route path="/login" component={Login} />
                </div>
            </Router>
        )
    }
}
export default App;

上面代码中,我们创建一个引用,然后当组件在加载的时候,会调用lookInnerRef函数,然后可以访问到底层的组件引用。

Redirect组件

react路由组件之Redirect组件

Redirect组件用于重定向,当我们进行某些操作时,会先让登录才能操作,如果我们操作时还没有登录,那么就会重定向到登录页面进行登录操作。

import React, { Component } from 'react';
import { BrowserRouter as Router , Route , Switch , Link , withRouter , Redirect } from 'react-router-dom';

const Public = () => <h3>Public</h3>;

const Private = () => <h3>Private</h3>;

const auth = {
    isAuth : false,
    authorize (callback) {
        this.isAuth = true;
        setTimeout(callback , 200);
    },
    unauthroize (callback) {
        this.isAuth = false;
        callback(callback , 200);
    }
}

function PrivateRoute (props) {
    const Component = props.component;
    const path = props.path;
    return (
        <Route path={path} render={
            (props) => {
                return auth.isAuth ? <Component {...props} /> : <Redirect to={{
                    pathname : '/login',
                    state : {from : props.location}
                }} />
            }
        } />
    )
}

class Login extends Component {
    constructor (props) {
        super(props);
        this.state = {
            redirectTo : false
        }
    }
    login = () => {
        auth.authorize(() => {
            this.setState({
                redirectTo : true
            })
        })
    }
    render () {
        let redirectTo = this.state.redirectTo;
        // 获取从哪里重定向过来的路径对象
        let { from } = this.props.location.state;
        if (redirectTo) {
            return <Redirect to={from} />
        }
        return (
            <div>
                <p>你还没有授权,请授权再查看</p>
                <button onClick={this.login}>授权</button>
            </div>
        )
    }
}

const Authorize = withRouter(class Authorize extends Component {
    constructor (props) {
        super(props);
    }
    loginOut = () => {
        auth.unauthroize(() => {
            this.props.history.push('/');
        })
    }
    render () {
        return (
            <div>
                {
                    auth.isAuth ? (
                        <div>
                            <div>你已经授权,可以查看需要授权的内容!</div>
                            <button onClick={this.loginOut}>退出</button>
                        </div>
                    ) : null
                }
            </div>
        )
    }
})


class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Link to="/public">public</Link>
                    <Link to="/private">private</Link>
                    <Authorize />
                    <Route path="/public" component={Public} />
                    <Route path="/login" component={Login} />
                    <PrivateRoute path="/private" component={Private} />
                </div>
            </Router>
        )
    }
}

上面是一个组件的简单应用,当我们渲染一个组件时,该组件会导航到一个新的地址。这个新的地址会在history栈中覆盖当前的地址,就像我们在服务端重定向一样。

import React, { Component } from 'react';
import { BrowserRouter as Router , Route , Switch , Link , withRouter , Redirect } from 'react-router-dom';

const auth = {
    isLogin : false
}

const Home = () => <h2>home</h2>;
// 这里我们使用withRouter高阶函数来包装组件,不然的话,我们获取不到Router的三个属性(history , location,match)
// 当点击login按钮时,首先会将isLogin的状态变为true,然后调用history的push方法,让url跳转到'/'
// 路由地址发生改变,组件会重新渲染,并且会重定向到"/home",然后匹配到正确的组件
// 这里需要注意的是,这里的history对象和浏览器的history对象类似。
const Login = withRouter(({history}) => {
    const login = () => {
        auth.isLogin = true;
        history.push('/');
    }
    return (
        <div>
            <button onClick={() => login()}>login</button>
        </div>
    )
});
class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Route exact path="/" render={() => (
                        auth.isLogin ? (
                            <Redirect to="/home"/>
                        ) : (
                            <Login />
                        )
                        )}/>
                    <Route path="/home" component={Home} />
                </div>
            </Router>
        )
    }
}
export default App;

上面这个例子中,如果我们把"exact"属性去掉会怎么样呢?其实当我们点击按钮进行重定向的时候会提示警告

Warning: You tried to redirect to the same route you're currently on: "/home"

这是因为当我们路由跳转到"/home"时,会同时匹配到"/"和"/home"。所以就会出现上面的警告提示,只需要在的组件上添加"exact"属性即可。

Redirect组件有以下几个属性:

  • 1、to属性
    • 这个属性可以是字符串也可以是对象,如果是字符串,那么表示的是重定向的url地址,如果是对象(location对象),那么对象的pathname属性表示的是重定向的url地址。
  • 2、from属性
    • 这个属性表示Redirect组件的路由原始值,当路由路径匹配from时,那么会重定向到to上面。如果Redirect组件上没有from属性,那么他都会匹配到当前路由的路径,这样的话,不管路由路径是哪个,都会重定向
  • 3、push属性
    • 该属性是一个布尔值,如果为true,表示把新的地址添加到访问历史记录里面作为入口,并且无法回退到前面的页面
  • 3、exact属性
    • 该属性是一个布尔值,如果为true,那么表示不能匹配路径的子路径,比如:当路径匹配"/home",就不能匹配"/home/one"。如果为false,那么就可以匹配,所以我们在使用路由的时候会添加,不然都会匹配到"/"。
  • 4、strict属性
    • 该属性是一个布尔值,如果为true,表示会匹配路径末尾的那个斜杠,比如:path为"/home/",不能匹配"/home",可以匹配"/home/",当然这个只匹配末尾的那个斜杠,如果斜杠后面还有参数,是不会受影响的,比如:path为"/home/",会匹配"/home/one"。

这个需要注意的是,如果想要保住路径末尾一定不会有斜杠,那么exact属性和strict属性两个必须为true。

import React, { Component } from 'react';
import { BrowserRouter as Router , Route , Switch , Link , withRouter , Redirect } from 'react-router-dom';

const auth = {
    isLogin : false
}

const Home = (props) => {
    console.log(props);
    return (
        <h2>home</h2>
    )
};
const Login = withRouter(({history}) => {
    const login = () => {
        auth.isLogin = true;
        history.push('/');
    }
    return (
        <div>
            <button onClick={() => login()}>login</button>
        </div>
    )
});

class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Route exact path="/" render={(props) => {
                        return (
                            auth.isLogin ? (
                                <Redirect to={{
                                    pathname : '/home',
                                    name : 'andy',
                                    search : "?city=广州",
                                    state : {referrer : props.location}
                                }} />
                            ) : (
                                <Login />
                            )
                        )
                    }}/>
                    <Route path="/home" component={Home} />
                </div>
            </Router>
        )
    }
}
export default App;

上面的这个例子中,我们使用组件的to属性来重定向到指定的路径,这里的to属性是一个对象,该对象其实是一个location对象,除了包含location对象中的属性外,我们还可以自定义属性。这里重定向的路径都是符合path-to-regexp库标准的。

location : {
    hash: "",   // #号后面的值
    key: "f1t7fc",
    name: "andy",
    pathname: "/home",
    search: "?city=广州",  // 查询字符串
    state : {
        referrer : {
            hash: "",
            key: "uevpb4",
            pathname: "/",
            search: "",
            state: undefined
        }
    }
}

下面这个例子是一个from属性的应用的例子,当路由路径匹配到from属性的值时,会重定向到to属性的值上

import React, { Component } from 'react';
import { BrowserRouter as Router , Route , Switch , Link , withRouter , Redirect } from 'react-router-dom';

const About = (props) => {
    console.log(props);
    return (
        <div>about</div>
    )
}
const Home = () => <div>home</div>

// 如果<Redirect>组件将from属性删掉,那么不管是哪个路由路径,它都会匹配到,所以也都会重定向到to上
class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Switch>
                        <Redirect from="/home" to="/about" />
                        <Route path="/about" component={About} />
                    </Switch>
                </div>
            </Router>
        )
    }
}

export default App;

Redirect组件综合例子

import React, { Component } from 'react';
import { BrowserRouter as Router , Route , Switch , Link , withRouter , Redirect } from 'react-router-dom';

// 这里我们调用withRouter高阶函数来将组件进行包装,主要目的是为了获取路由对象的三个属性(history , location , match)
const AddressBar = withRouter(class AddressBar extends Component {
    constructor (props) {
        super(props);
    }
    prevPage = () => {
        const { history } = this.props;
        history.goBack();
    }
    nextPage = () => {
        const { history } = this.props;
        history.goForward();
    }
    render () {
        return (
            <div>
                <button onClick={this.prevPage}>上一页</button>
                <button onClick={this.nextPage}>下一页</button>
                <div>URL : {this.props.location.pathname}</div>
            </div>
        )
    }
});

const isLogin = false;

class Nav extends Component {
    constructor (props) {
        super(props);
    }
    render () {
        return (
            <div>
                <Link className="link" to="/">Home</Link>
                <Link className="link" to="/old/123">Old</Link>
                <Link className="link" to="/new/456">New</Link>
                <Link className="link" to="/redirect/789">Redirect</Link>
                <Link className="link" to="/protected">Login</Link>
            </div>
        )
    }
}

class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <AddressBar />
                    <Nav />
                    <Route path="/" exact render={() => <h1>Home</h1>} />
                    <Switch>
                        // 当匹配from路径,那么会重定向到to路径上面
                        <Redirect from="/old/:id" to="/new/123" />
                        <Route path="/new/:id" render={({match}) => (
                            <h1>new : {match.params.id}</h1>
                        )} />
                    </Switch>
                    // push属性为true,添加新的地址作为访问历史记录的入口,这样就不能访问该路径之前的地址了
                    // push属性默认为false
                    <Route path="/redirect/:id" render={({match}) => (
                        <Redirect push to={`/new/${match.params.id}`} />
                    )} />
                    <Route path="/protected" render={() => (
                        isLogin ? <h1>Welcome</h1> : <Redirect to="/new/login" />
                    )} />
                </div>
            </Router>
        )
    }
}
export default App;

浅拷贝和深拷贝

浅拷贝和深拷贝

浅拷贝:如果我们需要拷贝的数据是一个基本类型,那么是值的拷贝,如果我们需要拷贝的数据是一个引用类型,那么是引用的拷贝,也就是说,原来的引用类型数据的改变会影响拷贝的数据。

深拷贝:如果我们拷贝的是一个数组或对象,那么会重新拷贝数组中的每一个元素或对象自己拥有的每一个属性。原来的数组或对象的改变不会影响新的数组或对象。

浅拷贝

当我们需要重新拷贝一个数组的副本时,我们一般都是调用数组的slice或concat方法来获取一个数组的副本,比如:

var arr1 = [1 , 'andy' , true , undefined , null];
var arr2 = arr1.slice();
arr1[0] = 2;
console.log(arr2)
console.log(arr1)

结果中,可以发现,当我们改变一个原数组的元素时,数组的副本并不会受到影响。然而当我们需要拷贝的数组中嵌套了对象或数组时,数组的副本就会受到影响,比如:

var arr1 = [1 , 'andy' , true , undefined , null , {name : 'andy'} , ['andy']];
var arr2 = arr1.slice();
arr1[5]['name'] = 'peter';
arr1[6][0] = 'jack';
console.log(arr2)
console.log(arr1)

结果中,当我们改变原数组中的对象的name属性的值和嵌套的数组的元素时,拷贝的数组中的值也会一同改变,这就是因为数组的浅拷贝只是拷贝数组或对象的引用,其实对于嵌套的对象或数组来说,原数组和拷贝的数组两者都保存的是同一个引用,而引用的指向都是一样的。

所以当我们再拷贝一个数组的时候,还是要根据当时数组的元素来进行,如果都是基本类型,那么我们可以使用数组的slice或concat方法来进行浅拷贝,如果需要改变数组的元素,那么我们就只能使用深拷贝,让原数组和拷贝的数组完全分隔开来,彼此操作互不影响。

深拷贝

我们可以使用JSON.stringify()将需要拷贝的数组转为json字符串,然后再调用JSON.parse()方法将json字符串转为json对象。比如:

var arr1 = [1 , 'andy' , true , undefined , null , {name : 'andy'} , ['andy'] , function () {}];
var arr2 = JSON.parse(JSON.stringify(arr1));
arr1[5]['name'] = 'jack';
arr1[6][0] = 'peter';
console.log(arr1)
console.log(arr2);

当然通过这样的方式也会存在缺陷,比如说如果数组中包含函数,那么会被转为null,数组中包含undefined,会被转为null,数组中包含日期对象,那么会被转为日期字符串等。所以当我们需要对数组进行拷贝的时候,一定要知道数组的数据结构都有什么类型的数据,然后使用相应的方式来进行拷贝。

深拷贝的实现

深拷贝的实现也不是很难,主要是遍历对象中的每一个元素,一个一个的进行拷贝,如果遇到对象的属性是一个对象时,就进行递归遍历每一项。

function deepCopy (obj) {
	var res = obj instanceof Array ? [] : {};
	for (var key in obj) {
		if (obj.hasOwnProperty(key)) {
			if (obj[key] !== null && typeof obj[key] === 'object') {
				res[key] = deepCopy(obj[key]);
			} else {
				res[key] = obj[key];
			}
		}
	};
	return res;
};

通过上面的代码,我们就可以简单的实现一个对象的深拷贝。

例子:

var arr1 = [1 , 'andy' , true , undefined , null , {name : 'andy'} , ['andy'] , function () {}];

function deepCopy (obj) {
	var res = obj instanceof Array ? [] : {};
	for (var key in obj) {
		if (obj.hasOwnProperty(key)) {
			if (obj[key] !== null && typeof obj[key] === 'object') {
				res[key] = deepCopy(obj[key]);
			} else {
				res[key] = obj[key];
			}
		}
	};
	return res;
};

var arr2 = deepCopy(arr1);
arr1[5]['name'] = 'jack';
console.log(arr1)
console.log(arr2)

React虚拟DOM

React虚拟DOM

这里有一个问题:什么是虚拟DOM?

什么是虚拟DOM?

虚拟DOM其实只是一个普通的javascript对象,里面包含了与真实DOM一一对应的属性。

看个例子:

class App extends Component {
    constructor (props) {
        super(props);
    }
    render () {
        return (
            <div>hello</div>
        )
    }
};

上面代码中,我们定义了一个React组件,在babel中,会将组件进行编译,其实调用的是:React.createElement(type , [props] , [...children])方法

function createElement(type, config, children) {
  var propName = void 0;

  // Reserved names are extracted
  var props = {};

  var key = null;
  var ref = null;
  var self = null;
  var source = null;

  if (config != null) {
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // Remaining properties are added to a new props object
    for (propName in config) {
      if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
        props[propName] = config[propName];
      }
    }
  }

  // 添加react元素的children属性
  var childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    var childArray = Array(childrenLength);
    for (var i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }
    props.children = childArray;
  }

  // 添加React组件的默认属性
  if (type && type.defaultProps) {
    var defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  {
    if (key || ref) {
      var displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type;
      if (key) {
        defineKeyPropWarningGetter(props, displayName);
      }
      if (ref) {
        defineRefPropWarningGetter(props, displayName);
      }
    }
  }
  return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}
var ReactElement = function (type, key, ref, self, source, owner, props) {
  var element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner
  };

  {
    element._store = {};
    Object.defineProperty(element._store, 'validated', {
      configurable: false,
      enumerable: false,
      writable: true,
      value: false
    });
    Object.defineProperty(element, '_self', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: self
    });
    Object.defineProperty(element, '_source', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: source
    });
    if (Object.freeze) {
      Object.freeze(element.props);
      Object.freeze(element);
    }
  }
  return element;
};

通过上面的代码,返回了一个react元素。打印一下react元素内部:

image

其实我们可以看到,react的虚拟DOM就是一个普通的js对象。

React之store介绍

Redux之store

我们都知道Redux是一个数据状态管理器,那么存放数据状态的容器在哪里呢?store就是存放数据状态的容器。

创建store

通过Redux的源码,我们来看一下store是怎么被创建的。

function createStore(reducer, preloadedState, enhancer) {
    var _ref2;
    // 验证参数
    if (typeof preloadedState === 'function' && typeof enhancer === 'function' || typeof enhancer === 'function' && typeof arguments[3] === 'function') {
        throw new Error('It looks like you are passing several store enhancers to ' + 'createStore(). This is not supported. Instead, compose them ' + 'together to a single function');
    }
    // 参数重载
    if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
        enhancer = preloadedState;
        preloadedState = undefined;
    }
    // 如果enhancer不为空,那么就返回一个增强的store,其实就是添加了中间件的store
    if (typeof enhancer !== 'undefined') {
        if (typeof enhancer !== 'function') {
            throw new Error('Expected the enhancer to be a function.');
        }
    
        return enhancer(createStore)(reducer, preloadedState);
    }

    if (typeof reducer !== 'function') {
        throw new Error('Expected the reducer to be a function.');
    }

    var currentReducer = reducer;    // 通过参数传入的reducer函数
    var currentState = preloadedState;    // 初始状态
    var currentListeners = [];    // 订阅函数集合
    var nextListeners = currentListeners;   
    var isDispatching = false;   // 是否正在派发action

    function ensureCanMutateNextListeners() {
        if (nextListeners === currentListeners) {
            nextListeners = currentListeners.slice();
        }
    }
    
    // 调用getState方法,返回当前的数据状态
    function getState() {
        if (isDispatching) {
            throw new Error('You may not call store.getState() while the reducer is executing. ' + 'The reducer has already received the state as an argument. ' + 'Pass it down from the top reducer instead of reading it from the store.');
        }
    
        return currentState;
    }
    // 订阅状态变化
    // 当状态发生变化的时候,就会调用listener
    // 调用这个方法时,返回一个unsubcribe函数,这个函数的作用就是用来取消订阅的。
    // 该方法内部其实就是将监听器添加到一个数组中,等到状态改变时,再依次拿出来调用
    function subscribe(listener) {
        if (typeof listener !== 'function') {
            throw new Error('Expected the listener to be a function.');
        }
    
        if (isDispatching) {
            throw new Error('You may not call store.subscribe() while the reducer is executing. ' + 'If you would like to be notified after the store has been updated, subscribe from a ' + 'component and invoke store.getState() in the callback to access the latest state. ' + 'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.');
        }
    
        var isSubscribed = true;
        ensureCanMutateNextListeners();
        nextListeners.push(listener);
        return function unsubscribe() {
            if (!isSubscribed) {
                return;
            }
    
            if (isDispatching) {
                throw new Error('You may not unsubscribe from a store listener while the reducer is executing. ' + 'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.');
            }
    
            isSubscribed = false;
            ensureCanMutateNextListeners();
            var index = nextListeners.indexOf(listener);
            nextListeners.splice(index, 1);
        };
    }
    
    // 该方法是用来分发任务的
    // 该方法主要执行以下几个步骤:
    // 1、调用reducer函数,执行状态更新操作
    // 2、调用所有订阅状态变化的监听器
    function dispatch(action) {
        if (!isPlainObject(action)) {
            throw new Error('Actions must be plain objects. ' + 'Use custom middleware for async actions.');
        }
    
        if (typeof action.type === 'undefined') {
            throw new Error('Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?');
        }
    
        if (isDispatching) {
            throw new Error('Reducers may not dispatch actions.');
        }
    
        try {
            isDispatching = true;
            currentState = currentReducer(currentState, action);
        } finally {
            isDispatching = false;
        }
    
        var listeners = currentListeners = nextListeners;
    
        for (var i = 0; i < listeners.length; i++) {
            var listener = listeners[i];
            listener();
        }
    
        return action;
    }
    // 省略代码...
    return _ref2 = {
        dispatch: dispatch,
        subscribe: subscribe,
        getState: getState,
        replaceReducer: replaceReducer
    }, _ref2[$$observable] = observable, _ref2;
}

创建store代码:

import { createStore } from 'redux';
const reducer = function (state = 0 , action) {
    switch (action.type) {
        case 'PLUS' : 
        return state + 1;
        case 'MINUS' :
        return state - 1;
        default :
        return state;
    }
};
const store = createStore(reducer);
store.subscribe(function () {
    console.log(store.getState());
})
store.dispatch({
    type : 'PLUS'
})

小结:

1、store是通过调用createStore方法来创建的,store是一个对象,该对象存在几个方法,其中getState方法主要是用来获取当前状态的,dispatch方法主要是用来派发任务并调用reducer函数改变状态的,subscribe方法主要是用来添加状态发生改变时需要调用的监听器的。

2、如果想改变状态,必须要派发一个任务,不然状态是不会发生改变。

new运算符

new运算符

new运算符,会创建一个用户定义的对象类型的实例或具有构造函数的内置对象的时候。

function Foo () {
    
}
new Foo();

当执行new Foo()时,内部到底做了什么事情?

  • 1、创建一个新对象,并且新对象继承自Foo.prototype
  • 2、使用指定的参数调用Foo构造函数,并将this绑定到新创建的对象
  • 返回这个新对象,如果构造函数有显式的返回一个对象,那么这个对象会覆盖之前创建的对象,如果显式的返回一个基本类型,那么会忽略返回值,还是返回新对象

模拟实现new来创建对象

首先我们创建一个创建对象的工厂函数,接收的第一个参数是一个函数(构造函数),其他参数就是调用函数时传入的参数。

function createObjectFactory (Cont) {
    
}

从第一点可以看出,内部会创建一个新对象,并且该对象继承自函数的原型对象

function createObjectFactory (Cont) {
    var obj = new Object();
    var args = Array.prototype.slice.call(arguments , 1);
    obj.__proto__  = Cont.prototype;
}

调用构造函数,并绑定this到新创建的对象。

function createObjectFactory (Cont) {
    var obj = new Object();
    var args = Array.prototype.slice.call(arguments , 1);
    obj.__proto__  = Cont.prototype;
    var res = Cont.apply(obj , args);
}

如果构造函数没有返回值,那么就返回这个新对象,如果有返回值并且这个返回值是一个对象,那么就返回这个对象,如果是基本类型,那么就忽略,还是返回这个新对象

function createObjectFactory (Cont) {
    var obj = new Object();
    var args = Array.prototype.slice.call(arguments , 1);
    obj.__proto__ = Cont.prototype;
    var res = Cont.apply(obj , args);
    return typeof res === 'object' ? res : obj;
}

react的事件机制

react的事件机制

react的事件机制,主要分为两个阶段:事件注册和事件分发。当我们渲染完FiberNode tree,创建真实DOM,并建立虚拟DOM和真实DOM之间的联系后,就会调用setInitialDOMProperties方法,为DOM添加属性和事件,其实react的事件注册就是从这里开始的。

举个例子:

// 这个例子中,既有事件冒泡阶段,也有事件捕获阶段
// event._dispatchListeners中保存了所有事件回调函数(包括捕获阶段和冒泡阶段)
// event._dispatchInstances中保存了当前事件的目标元素及其父元素(FiberNode)
class App extends Component {
    constructor (props) {
        super(props);
    }
    add = (evt) => {
        console.log(1);
    }
    add1 = () => {
        console.log(23);
    }
    add2 = () => {
        console.log('capture');
    }
    delete = () => {
        console.log('delete');
    }
    render () {
        return (
            <div>
                <div onClick={this.add1} onClickCapture={this.add2}>
                    <button onClick={this.add}>add</button>
                    <button onClick={this.delete}>delete</button>
                </div>
            </div>
        )
    }
}
export default App;

打印event._dispatchListeners和event._dispatchInstances的结果

image

事件注册

以上面的例子为例,我们来看一下button这个元素对应的虚拟DOM中的props对象有哪些属性:

image

当调用setInitialDOMProperties方法,该方法内部会遍历虚拟DOM中的props,给最终要渲染的DOM添加一系列的属性,比如:style,class,text,innerHTML,autoFocus,event等。

如果是prop是函数的话,那么就会执行ensureListeningTo这个方法,我们来看一下这个方法内部:

function ensureListeningTo(rootContainerElement, registrationName) {
    // 其实这个方法里面就是判断当前的这个DOM是不是document或者fragment
    // 如果不是的话,那么就会获取当前DOM的document,然后将事件委托到document上
    var isDocumentOrFragment = rootContainerElement.nodeType === DOCUMENT_NODE || rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
    var doc = isDocumentOrFragment ? rootContainerElement : rootContainerElement.ownerDocument;
    listenTo(registrationName, doc);
}

在listenTo方法中,除了scroll,focus,blur,cancel,close方法走trapCapturedEvent方法,invalid,submit,reset方法不处理之外,剩下的事件类型全走default,执行trapBubbledEvent这个方法,trapCapturedEvent和trapBubbledEvent二者唯一的不同之处就在于,对于最终的合成事件,前者注册捕获段的事件监听器,而后者则注册冒泡阶段的事件监听器。

我们来看一下trapBubbledEvent方法:

function trapBubbledEvent(topLevelType, element) {
    if (!element) {
        return null;
    }
    var dispatch = isInteractiveTopLevelEventType(topLevelType) ? dispatchInteractiveEvent : dispatchEvent;
    // 其实这个方法内部就是调用DOM的addEventListener来注册事件
    addEventBubbleListener(element, getRawEventName(topLevelType),
    // Check if interactive and wrap in interactiveUpdates
    dispatch.bind(null, topLevelType));
}
function addEventBubbleListener(element, eventType, listener) {
    element.addEventListener(eventType, listener, false);
}

其实addEventBubbleListener方法内部就是一个DOM调用addEventListener方法来注册事件,而事件的回调函数就是dispatch.bind(null, topLevelType)返回的函数。

上面的流程基本上就是react事件注册的流程。基本上在事件注册流程中,主要做的就是事件的兼容,以及将事件委托到document上。

事件分发

当所有的事件都委托到了document上,那么事件触发的时候,就需要一个分发的过程,来找到哪个元素触发了事件,并且执行相应的回调函数。当触发事件的时候会调用dispatchEvent。

dispatchEvent

dispatchEvent方法中,有这么一段代码:

var nativeEventTarget = getEventTarget(nativeEvent);
var targetInst = getClosestInstanceFromNode(nativeEventTarget);

代码的意思就是找到触发事件的DOM和DOM对应的React元素。而我们在组件渲染的时候都知道真实的DOM和React元素之间是通过internalInstanceKey来向关联的。所以我们通过DOM的internalInstanceKey属性就可以找到对应的React元素。

查找真实DOM是比较好找的,直接通过event.target || event.srcElement || window;如果点击的是文本节点,那么就找到文本节点的父节点。

batchedUpdates

batchedUpdates方法的字面意思就是批处理更新,其实内部就是向上遍历节点,找到该节点的所有父节点并保存在ancestors数组中,主要是因为事件回调处理可能会改变DOM,所以要在调用事件回调函数之前,把触发事件的节点的所有的父节点都保存起来,就是为了防止在调用回调的时候会改变DOM结构,导致与React缓存的节点不一致。

batchedUpdates方法内部主要是调用handleTopLevel方法:

function handleTopLevel(bookKeeping) {
    // 找到触发事件的DOM对应的FiberNode
    var targetInst = bookKeeping.targetInst;
    var ancestor = targetInst;
    // 向上遍历FiberNode,并将FiberNode的父节点保存在ancestors数组中
    do {
        if (!ancestor) {
          bookKeeping.ancestors.push(ancestor);
          break;
        }
        var root = findRootContainerNode(ancestor);
        if (!root) {
          break;
        }
        bookKeeping.ancestors.push(ancestor);
        ancestor = getClosestInstanceFromNode(root);
    } while (ancestor);
    // 循环ancestors数组,并调用runExtractedEventsInBatch方法
    // 这里的for循环是从前往后遍历,也就是说,先是会执行当前节点,然后是当前节点的父节点,以此类推。
    for (var i = 0; i < bookKeeping.ancestors.length; i++) {
        targetInst = bookKeeping.ancestors[i];
        runExtractedEventsInBatch(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
    }
}
runExtractedEventsInBatch

runExtractedEventsInBatch方法接受四个参数:

  • 事件类型
  • 触发事件的当前DOM对应的FiberNode节点
  • 原生的事件对象
  • 触发事件的目标对象(DOM节点)

runExtractedEventsInBatch方法,从字面意思上来讲,就是执行在更新队列中提取的事件,这个方法内部先是执行extractEvents方法。当我们再看extractEvents方法内部时,首先要了解内部出现的plugins是什么?我们打印一下:

image

其实plugins就是一个数组,里面保存的就是各种不同事件类型集合,比如:一些简单的事件类型,鼠标事件,change事件,select事件,beforeInput事件等。

知道plugins是一个数组,里面保存的就是各种事件类型集合,那么我们就要找一下这个plugins具体是怎么来的。

源码中有这么一段代码:

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


interactiveEventTypeNames.forEach(function (eventTuple) {
  addEventTypeNameToConfig(eventTuple, true);
});
nonInteractiveEventTypeNames.forEach(function (eventTuple) {
  addEventTypeNameToConfig(eventTuple, false);
});

interactiveEventTypeNames表示的是:交互式的事件类型集合,nonInteractiveEventTypeNames:表示的是:非交互式的事件类型集合。遍历这两个数组,并将事件类型名称作为属性,映射到一个对象上,具体的结构如下:

image

// 结构如下:
{
    click : {
        phasedRegistrationNames : xxx,
        dependencies : xxx,
        isInteractive : xxx
    }
}

我们再来看一下这段代码:

// 依赖注入的方法
var injection = {
  injectEventPluginOrder: injectEventPluginOrder,
  injectEventPluginsByName: injectEventPluginsByName
};
// 将这五种eventPlugin依赖注入到eventPluginHub中
injection.injectEventPluginsByName({
    SimpleEventPlugin: SimpleEventPlugin,
    EnterLeaveEventPlugin: EnterLeaveEventPlugin,
    ChangeEventPlugin: ChangeEventPlugin,
    SelectEventPlugin: SelectEventPlugin,
    BeforeInputEventPlugin: BeforeInputEventPlugin
});

injection.injectEventPluginOrder(DOMEventPluginOrder);

执行上面这两个方法,内部都会调用recomputePluginOrdering方法。执行完这两个方法会分别将值保存到namesToPlugins和eventPluginOrder两个变量中。

namesToPlugins:

image

eventPluginOrder:

image

// 这个方法内部是通过for循环,遍历plugins,然后将每一个plugin保存到namesToPlugins这个对象上,最后调用recomputePluginOrdering方法
function injectEventPluginsByName(injectedNamesToPlugins) {
  var isOrderingDirty = false;
  for (var pluginName in injectedNamesToPlugins) {
    if (!injectedNamesToPlugins.hasOwnProperty(pluginName)) {
      continue;
    }
    var pluginModule = injectedNamesToPlugins[pluginName];
    if (!namesToPlugins.hasOwnProperty(pluginName) || namesToPlugins[pluginName] !== pluginModule) {
      !!namesToPlugins[pluginName] ? invariant(false, 'EventPluginRegistry: Cannot inject two different event plugins using the same name, `%s`.', pluginName) : void 0;
      namesToPlugins[pluginName] = pluginModule;
      isOrderingDirty = true;
    }
  }
  if (isOrderingDirty) {
    recomputePluginOrdering();
  }
}

最后调用recomputePluginOrdering方法,通过namesToPlugins和eventPluginOrder重新计算plugins。

plugins :

image

extractEvents

extractEvents方法内部就是循环plugins数组,然后调用相应plugin的extractEvents方法,而这个方法就是用来构造合成事件的,我们这里以click事件为例,click事件对应的plugin就是SimpleEventPlugin,那么就会调用SimpleEventPlugin.extractEvents方法,在这个方法内部会通过事件类型,来判断具体执行哪个合成事件的构造函数。而click事件对应的合成事件的构造函数就是SyntheticMouseEvent,SyntheticMouseEvent是SyntheticUIEvent的子类,SyntheticUIEvent是SyntheticEvent的子类,它们之间是通过SyntheticEvent的extends方法来实现继承的。

SyntheticEvent.extend = function (Interface) {
    var Super = this;
    
    var E = function () {};
    E.prototype = Super.prototype;
    var prototype = new E();
    
    function Class() {
        return Super.apply(this, arguments);
    }
    _assign(prototype, Class.prototype);
    Class.prototype = prototype;
    Class.prototype.constructor = Class;
    
    Class.Interface = _assign({}, Super.Interface, Interface);
    Class.extend = Super.extend;
    addEventPoolingTo(Class);
    
    return Class;
};

上面代码中,extend方法内部使用的是寄生组合式继承。让EventConstructor继承SyntheticEvent上的属性和方法。

getPooledEvent

该方法是从事件对象池中提取事件,将事件缓存在对象池中,可以降低对象的创建和销毁时间,提高性能。

function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) {
  var EventConstructor = this;
  if (EventConstructor.eventPool.length) {
    var instance = EventConstructor.eventPool.pop();
    EventConstructor.call(instance, dispatchConfig, targetInst, nativeEvent, nativeInst);
    return instance;
  }
  return new EventConstructor(dispatchConfig, targetInst, nativeEvent, nativeInst);
}

第一次触发click事件的时候,事件对象池中是空的,对象池中没有对应的合成事件引用,那么就需要new EventConstructor来初始化合成事件对象,之后就不需要初始化了。直接通过eventPool.pop()来获取就可以了。当调用new EventConstructor这个子类,那么就会调用到父类SyntheticEvent的构造函数,开始构造合成事件,主要就是将原生浏览器事件上的属性和方法挂载到合成事件上,方法的话主要是preventDefault和stopPropagation。

我们知道react的事件都是委托到document上面,所以这里调用event.stopPropagation(),其实阻止的是事件继续向document或者fragment的父元素传播。那么react是怎么做到与原生浏览器事件一样的行为呢?我们这里看一下stopPropagation是怎么处理的:

  stopPropagation: function () {
    var event = this.nativeEvent;
    if (!event) {
      return;
    }

    if (event.stopPropagation) {
      event.stopPropagation();
    } else if (typeof event.cancelBubble !== 'unknown') {
      event.cancelBubble = true;
    }
    this.isPropagationStopped = functionThatReturnsTrue;
  },

上面代码中,比较简单,获取事件对象,然后调用事件对象的stopPropagation()来阻止事件向上冒泡,最重要的是最后一句代码,相当于是设置了一个标志位,对于冒泡事件来说,当事件触发,由子元素往父元素逐级向上遍历,会按顺序执行每层元素对应的事件回调,但如果发现当前元素对应的合成事件上的 isPropagationStopped为true值,则遍历的循环将中断,也就是不再继续往上遍历,当前元素的所有父元素的合成事件就不会被触发,最终的效果,就和浏览器原生事件调用 e.stopPropagation()的效果是一样的。

accumulateTwoPhaseDispatches

当获取到合成事件对象后,就会调用accumulateTwoPhaseDispatches方法

image

如上图,accumulateTwoPhaseDispatches方法内部具体做了哪些事情:

  • 调用forEachAccumulated方法
    • 变量所有的合成事件,执行accumulateTwoPhaseDispatchesSingle方法
  • 调用accumulateTwoPhaseDispatchesSingle方法
    • 检查当前事件是否具有冒泡阶段或捕获节点,如果有,那么就调用traverseTwoPhase方法
  • 调用traverseTwoPhase方法
    • 从当前元素开始向上遍历当前元素所有父元素,并将其保存在数组中,并且分别按照事件捕获和事件冒泡的顺序执行accumulateDirectionalDispatches方法
  • 调用accumulateDirectionalDispatches方法
    • 调用listenerAtPhase方法获取对应的事件回调函数。
    • 将事件回调函数和实例(这里的实例其实是一个FiberNode)挂载到事件对象的属性上。

当拿到与事件相关的实例和回调函数之后,那么就可以对合成事件进行批量处理了。

runEventsInBatch
function runEventsInBatch(events) {
    // 如果当前事件存在,那么就将当前事件和之前还没有处理完毕的事件进行合并,组成一个新的事件队列
    if (events !== null) {
        eventQueue = accumulateInto(eventQueue, events);
    }
    
    var processingEventQueue = eventQueue;
    eventQueue = null;
    
    if (!processingEventQueue) {
        return;
    }
    
    forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
    !!eventQueue ? invariant(false, 'processEventQueue(): Additional events were enqueued while processing an event queue. Support for this has not yet been implemented.') : void 0;
    rethrowCaughtError();
}

runEventsInBatch方法,首先会调用accumulateInto方法将当前需要处理的事件和之前还没有处理完毕的事件队列合并在一起,组成一个新的事件队列。

然后调用forEachAccumulated方法,在这个方法中,会判断processingEventQueue这个事件队列是不是一个数组,如果是一个数组,那么就遍历这个数组,调用executeDispatchesAndReleaseTopLevel方法,如果不是一个数组,那么就直接调用executeDispatchesAndReleaseTopLevel方法。

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

var executeDispatchesAndRelease = function (event) {
  if (event) {
    executeDispatchesInOrder(event);

    if (!event.isPersistent()) {
      event.constructor.release(event);
    }
  }
};

executeDispatchesAndReleaseTopLevel方法内部调用executeDispatchesAndRelease方法,而executeDispatchesAndRelease方法内部会先调用executeDispatchesInOrder方法

executeDispatchesInOrder
function executeDispatchesInOrder(event) {
    // 获取事件捕获阶段和事件冒泡阶段的所有事件回调函数
    var dispatchListeners = event._dispatchListeners;
    // 获取事件对应的FiberNode
    var dispatchInstances = event._dispatchInstances;
    {
        validateEventDispatches(event);
    }
    if (Array.isArray(dispatchListeners)) {
        for (var i = 0; i < dispatchListeners.length; i++) {
            if (event.isPropagationStopped()) {
                break;
            }
            executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
        }
    } else if (dispatchListeners) {
        executeDispatch(event, dispatchListeners, dispatchInstances);
    }
    event._dispatchListeners = null;
    event._dispatchInstances = null;
}

该方法中,先获取当前事件触发目标对象的事件捕获阶段和事件冒泡阶段的所有事件回调函数,以及事件对象对应的FiberNode。如果dispatchListeners是一个数组,那么就遍历这个数组,并且判断当前事件是否调用stopPropagation方法来阻止事件冒泡,如果有的话就退出循环,然后调用executeDispatch方法,如果dispatchListeners不是一个数组并且存在的话,那么就直接调用executeDispatch方法。

executeDispatch
function executeDispatch(event, listener, inst) {
    var type = event.type || 'unknown-event';
    event.currentTarget = getNodeFromInstance(inst);
    invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
    event.currentTarget = null;
}

这个方法会获取事件类型,和事件的目标对象。然后一层层调用各种方法:

invokeGuardedCallbackAndCatchFirstError方法—>invokeGuardedCallback方法—>invokeGuardedCallbackImpl$1方法—>invokeGuardedCallbackImpl方法
invokeGuardedCallbackImpl
var invokeGuardedCallbackImpl = function (name, func, context, a, b, c, d, e, f) {
    var funcArgs = Array.prototype.slice.call(arguments, 3);
    try {
        func.apply(context, funcArgs);
    } catch (error) {
        this.onError(error);
    }
};

这里主要就是执行事件回调函数了,到此事件执行就完毕了。当事件执行完了之后,就会调用EventConstructor.release方法(就是调用releasePooledEvent方法)来释放事件对象并清理内存。

releasePooledEvent
function releasePooledEvent(event) {
    var EventConstructor = this;
    !(event instanceof EventConstructor) ? invariant(false, 'Trying to release an event instance into a pool of a different type.') : void 0;
    event.destructor();
    if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
        EventConstructor.eventPool.push(event);
    }
}

上面代码中,调用event.destructor方法清理掉event事件对象上的属性(常见的手动释放内存的方法就是将对象置为null),然后把清理后的event事件对象再放入到事件对象池中。

react组件间通信

react组件通信

react组件间的通信基本上可以分为三类:父子组件间的通信,爷孙组件间的通信,兄弟组件间的通信。

父子组件间的通信

react中,父子组件间的通信主要是通过props来实现。不管是父组件向子组件通信,还是子组件向父组件通信,它们都是通过props来实现。

  • 1、父组件向子组件通信
class Child extends Component {
  constructor (props) {
    super(props);
  }
  render () {
    const {name , age} = this.props;
    return (
      <div>
        <div>my name is {name}</div>
        <div>my age is {age}</div>
      </div>
    )
  }
}

class Parent extends Component {
  constructor (props) {
    super(props);
    this.state = {
      name : 'andy',
      age : 23
    }
  }
  render () {
    const {name , age} = this.state;
    return (
      <div>
        <Child name={name} age={age}></Child>
      </div>
    )
  }
}

上面代码中,父组件通过向子组件传递属性来实现与子组件的通信。

  • 2、子组件向父组件通信
class Child extends Component {
  constructor (props) {
    super(props);
  }
  render () {
    const { sendToParent } = this.props;
    return (
      <div>
        <button onClick={() => sendToParent('hello parent')}>向父组件发送消息</button>
      </div>
    )
  }
}

class Parent extends Component {
  constructor (props) {
    super(props);
    this.sendToParent = this.sendToParent.bind(this);
  }
  sendToParent (value) {
    console.log('子组件向父组件通信' , value);
  }
  render () {
    return (
      <div>
        <Child sendToParent={this.sendToParent}></Child>
      </div>
    )
  }
}

爷孙组件间的通信

在react中,爷孙组件指的是祖先组件与子组件之间的通信。

  • 1、context进行通信
import React, { Component } from 'react';

const ThemeContext = React.createContext({
  backgroundColor : 'red',
  color : 'blue'
});

class Parent extends Component {
  render () {
    return (
      <ThemeContext.Provider value={{backgroundColor : 'green' , color : 'red'}}>
        <Middle />
      </ThemeContext.Provider>
    )
  }
};

class Middle extends Component {
  render () {
    return (
      <Child />
    )
  }
};

class Child extends Component {
  render () {
    return (
      <ThemeContext.Consumer>
        {context => (
          <div>
            <h1 style={{'background-color' : context.backgroundColor}}>my name is andy</h1>
            <div style={{color : context.color}}>my age is 12</div>
          </div>
        )}
      </ThemeContext.Consumer>
    )
  }
}
class App extends Component {
  render() {
    return (
      <Parent />
    )
  }
}

export default App;
  • 2、通过eventBus来实现组件间通信
import React, { Component } from 'react';

class EventEmitter {
  constructor () {
    this.events = {}
  }
  subscribe (name , callback) {
    if (!this.events[name]) {
      this.events[name] = [];
    }
    this.events[name].push(callback);
  }
  publish (name , ...args) {
    let callbacks = this.events[name];
    for (let i = 0 ; i < callbacks.length ; i++) {
      callbacks[i].apply(this , args);
    }
  }
};

const event = new EventEmitter();

class Parent extends Component {
  constructor (props) {
    super(props);
  }
  componentDidMount () {
    event.subscribe('hello' , this.handleHello);
  }
  handleHello = (msg) => {
    console.log(msg);
  }
  sayChild = () => {
    event.publish('sayChild' , 'hello , my child');
  }
  render () {
    return (
      <div>
        <button onClick={this.sayChild}>父元素的按钮</button>
        <Middle></Middle>
      </div>
    )
  }
}

class Middle extends Component {
  render () {
    return (
      <Child></Child>
    )
  }
}

class Child extends Component {
  hello = () => {
    event.publish('hello' , 'hello , my parent');
  }
  sayChild = (msg) => {
    console.log(msg);
  }
  componentDidMount () {
    event.subscribe('sayChild' , this.sayChild);
  }
  render () {
    return (
      <div>
        <button onClick={this.hello}>子元素的按钮</button>
      </div>
    )
  }
}

class App extends Component {
  render() {
    return (
      <Parent />
    )
  }
}
export default App;

兄弟组件间通信

对于兄弟组件来说,一般都存在共同的父节点,可以通过将父节点作为桥梁来实现兄弟组件间的通信,除此之外还可以使用消息中间件,通过发布和订阅消息来实现兄弟组件之间的通信。

  • 1、利用共同父节点作为中转来实现兄弟组件间通信
class Brother1 extends Component {
    constructor (props) {
        super(props);
    }
    send = () => {
        const { send } = this.props;
        send('brother1');
    }
    render () {
        const { name } = this.props;
        return (
            <div>
                <h1>brother1</h1>
                <p>消息:{name}</p>
                <button onClick={this.send}>发消息给brother2</button>
            </div>
        )
    }
}

class Brother2 extends Component {
    constructor (props) {
        super(props);
    }
    send = () => {
        const { send } = this.props;
        send('brother2');
    }
    render () {
        const { name } = this.props;
        return (
            <div>
                <h1>brother2</h1>
                <p>消息:{name}</p>
                <button onClick={this.send}>发消息给brother1</button>
            </div>
        )
    }
}

class Parent extends Component {
    constructor (props) {
        super(props)
        this.state = {
            name : ''
        }
    }
    send = (value) => {
        this.setState({
            name : value
        });
    }
    render () {
        const { name } = this.state;
        return (
            <div>
                <Brother1 name={name} send={this.send} />
                <Brother2 name={name} send={this.send} />
            </div>
        )
    }
}
  • 2、通过消息中间件的方式来实现
class EventEmitter {
    constructor () {
        this.events = {}
    }
    subscribe (name , callback) {
        if (!this.events[name]) {
            this.events[name] = [];
        }
        this.events[name].push(callback);
    }
    publish (name , ...args) {
        let callbacks = this.events[name];
        for (let i = 0 ; i < callbacks.length ; i++) {
            callbacks[i].apply(this , args);
        }
    }
};
  
const event = new EventEmitter();

class Brother1 extends Component {
    constructor (props) {
        super(props);
        this.state = {
            name : ''
        }
    }
    send = () => {
        event.publish('brother1' , 'hello brother2');
    }
    componentDidMount () {
        event.subscribe('brother2' , value => {
            this.setState({
                name : value
            })
        })
    }
    render () {
        const { name } = this.state;
        return (
            <div>
                <h1>brother1</h1>
                <p>消息:{name}</p>
                <button onClick={this.send}>发消息给brother2</button>
            </div>
        )
    }
}

class Brother2 extends Component {
    constructor (props) {
        super(props);
        this.state = {
            name : ''
        }
    }
    send = () => {
        event.publish('brother2' , 'hello brother1');
    }
    componentDidMount () {
        event.subscribe('brother1' , value => {
            this.setState({
                name : value
            })
        })
    }
    render () {
        const { name } = this.state;
        return (
            <div>
                <h1>brother2</h1>
                <p>消息:{name}</p>
                <button onClick={this.send}>发消息给brother1</button>
            </div>
        )
    }
}

class Parent extends Component {
    constructor (props) {
        super(props)
    }
    send = (value) => {
        this.setState({
            name : value
        });
    }
    render () {
        return (
            <div>
                <Brother1 />
                <Brother2 />
            </div>
        )
    }
}

react的调度机制

react的调度机制

通过react v16版本的源码中,我们可以看到,对于react的任务调度的部分是放在schedular这个库中。不过这个调度机制是异步调度,目前我们使用react做的应用基本上都是同步的。

基础

在看schedular库中,有一些基础知识,我们需要先了解一下。

1、window.performance.now

这个方法表示的是从页面加载开始时,到当前之间的时间,单位为毫秒。

2、window.requestAnimationFrame

这个方法接受一个回调函数作为参数,表示的是推迟回调函数的执行,并且会推迟到浏览器在下一次重绘之前调用回调函数,也就是说该回调函数会在浏览器下一次重绘之前执行。(回调函数会接受一个参数,这个参数是一个时间戳,是performance.now()的返回值,表示的是从页面加载开始到当前的时间)。

如果想让浏览器在下一次重绘之前继续更新下一帧动画,那么可以在回调函数中再次调用window.requestAnimationFrame方法。

3、window.MessageChannel

这个接口允许我们创建一个新的消息通道,并通过它的两个MessagePort属性发送数据。

用法:

let ch = new MessageChannel();
ch.port1.postMessage('我是port1发送的消息');
ch.port2.postMessage('我是port2发送的消息');
ch.port1.onmessage = function (e) {
    console.log('这是port1接收的消息' , e.data);
}
ch.port2.onmessage = function (e) {
    console.log('这是port2接收的消息' , e.data);
}

调度

1、任务优先级

react对任务的优先级分为五种:

var ImmediatePriority = 1;      // 最高优先级
var UserBlockingPriority = 2;   // 用户阻塞优先级
var NormalPriority = 3;         // 普通优先级
var LowPriority = 4;            // 低优先级
var IdlePriority = 5;           // 空闲优先级

五种优先级所对应的过期时间:

var maxSigned31BitInt = 1073741823;

// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;   // 立马过期
// Eventually times out
var USER_BLOCKING_PRIORITY = 250;      // 250毫秒后过期
var NORMAL_PRIORITY_TIMEOUT = 5000;    // 5秒后过期
var LOW_PRIORITY_TIMEOUT = 10000;      // 10秒后过期
// Never times out
var IDLE_PRIORITY = maxSigned31BitInt;  // 永不过期

添加任务

react添加任务,我们需要了解,它是怎么添加的,添加到哪里的?我们来具体看一下代码中是怎么实现的:

function unstable_scheduleCallback(callback, deprecated_options) {
    // 返回的是一个调用performance.now()的时间戳
    var startTime = currentEventStartTime !== -1 ? currentEventStartTime : exports.unstable_now();

    // 过期时间
    var expirationTime;
    if (typeof deprecated_options === 'object' && deprecated_options !== null && typeof deprecated_options.timeout === 'number') {
        expirationTime = startTime + deprecated_options.timeout;
    } else {
        // 根据任务优先级来设置任务的过期时间
        switch (currentPriorityLevel) {
            case ImmediatePriority:
                expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
            break;
            case UserBlockingPriority:
                expirationTime = startTime + USER_BLOCKING_PRIORITY;
            break;
            case IdlePriority:
                expirationTime = startTime + IDLE_PRIORITY;
            break;
            case LowPriority:
                expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
            break;
            case NormalPriority:
            default:
            expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
        }
    }
    
    // 任务节点,
    var newNode = {
        callback: callback,    // 具体的任务内容
        priorityLevel: currentPriorityLevel,    // 当前任务的优先级
        expirationTime: expirationTime,    // 当前任务的过期时间
        next: null,    // 当前任务的下一个任务节点
        previous: null     // 当前任务的上一个任务节点
    };

    // 向链表中插入任务节点,通过任务的过期时间来排序
    // 如果不存在任务节点,说明这是第一个任务,所以这个新的任务节点就是第一个任务节点,并且该节点的next和previous都指向自己,形成双循环结构。
    // 如果已经存在任务节点,那么就会通过任务的过期时间去排序,如果当前任务的过期时间大于新任务的过期时间,那么说明新任务的过期时间在当前任务之前,所以新任务会更快执行,所以会把新任务放在当前任务的前面
    if (firstCallbackNode === null) {
        // This is the first callback in the list.
        firstCallbackNode = newNode.next = newNode.previous = newNode;
        ensureHostCallbackIsScheduled();
    } else {
        // 下一个任务的位置
        var next = null;
        var node = firstCallbackNode;
        do {
            if (node.expirationTime > expirationTime) {
                // The new callback expires before this one.
                next = node;
                break;
            }
            node = node.next;
        } while (node !== firstCallbackNode);
        
        // 找了一圈发现,没有一个任务的过期时间比新的任务大,那么就说明新任务应该是在链表的最后,所以新任务的下一个任务就是第一个任务
        if (next === null) {
            next = firstCallbackNode;
        } else if (next === firstCallbackNode) {
            // 找第一个的时候就找到了next,那么说明新的任务要放在第一个任务的前面
            firstCallbackNode = newNode;
            ensureHostCallbackIsScheduled();
        }
        
        // 任务之间的关联
        var previous = next.previous;
        previous.next = next.previous = newNode;
        newNode.next = next;
        newNode.previous = previous;
    }

    return newNode;
}

上面的代码就是如何插入任务,并且通过任务的过期时间,将这些任务都进行了排序,那任务是什么时候执行的呢?

通过上面代码中,我们可以看到有两种情况,第一种情况是:当添加第一个任务节点的时候开始启动任务执行,第二种情况是:当新添加的任务节点取代之前的任务节点称为新的第一个节点的时候,也会启动任务执行。这就是源码中的==ensureHostCallbackIsScheduled==方法执行的内容。

因为第一种情况表示的是,任务从无到有,所以应该立即执行。第二种情况表示的是,有新的优先级最高的任务,应该停止之前要执行的任务,重新从新的任务开始执行。

如何在浏览器每一帧绘制完的空闲时间来做一些事情?react使用的是requestAnimationFrame和MessageChannel来实现。

requestAnimationFrameWithTimeout方法

var requestAnimationFrameWithTimeout = function (callback) {
    rAFID = localRequestAnimationFrame(function (timestamp) {
        // cancel the setTimeout
        localClearTimeout(rAFTimeoutID);
        callback(timestamp);
    });
    rAFTimeoutID = localSetTimeout(function () {
        // cancel the requestAnimationFrame
        localCancelAnimationFrame(rAFID);
        callback(exports.unstable_now());
    }, ANIMATION_FRAME_TIMEOUT);
};

上面这段代码是什么意思呢?其实就是当我们调用requestAnimationFrameWithTimeout方法,并且传入一个callback参数的时候,会启动一个requestAnimationFrame和一个setTimeout,两个都会执行,但是由于requestAnimationFrame的执行优先级高于setTimeout,所以会先执行requestAnimationFrame,当执行requestAnimationFrame的时候,会调用localClearTimeout方法(其实就是clearTimeout方法)取消setTimeout定时器的执行,所以在页面激活的情况下,其实执行的就是requestAnimationFrame。

但是requestAnimationFrame方法,在页面切换到未激活的时候是不工作的,这时候requestAnimationFrameWithTimeout方法其实就启动了一个100毫秒的定时器,来执行任务。

ensureHostCallbackIsScheduled方法。

当我们将任务通过过期时间进行排序添加到链表中后,我们就要在合适的时机去执行这些任务,这里我们会调用ensureHostCallbackIsScheduled方法。

function ensureHostCallbackIsScheduled() {
    // 判断是否有正在执行的任务,如果有的话,那么就直接跳过
    if (isExecutingCallback) {
        return;
    }
    // 如果没有正在执行的任务,那么会从链表中取出最早过期的任务执行
    var expirationTime = firstCallbackNode.expirationTime;
    if (!isHostCallbackScheduled) {
        isHostCallbackScheduled = true;
    } else {
        cancelHostCallback();
    }
    // 执行任务
    requestHostCallback(flushWork, expirationTime);
}
// 这里主要是通过调用requestAnimationFrame来执行任务操作
requestHostCallback = function (callback, absoluteTimeout) {
    // 当前任务
    scheduledHostCallback = callback;
    // 当前任务的过期时间
    timeoutTime = absoluteTimeout;
    if (isFlushingHostCallback || absoluteTimeout < 0) {
        port.postMessage(undefined);
    } else if (!isAnimationFrameScheduled) {
      isAnimationFrameScheduled = true;
      requestAnimationFrameWithTimeout(animationTick);
    }
};
var animationTick = function (rafTime) {
    if (scheduledHostCallback !== null) {
        // 有任务再进行递归,没任务的话不需要工作
        requestAnimationFrameWithTimeout(animationTick);
    } else {
        isAnimationFrameScheduled = false;
      return;
    }
    // 下一帧开始时间,其实就等于当前帧的开始时间加上一帧的渲染时间再减去当前帧的截止时间
    var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
    // 如果下一帧的开始时间小于一帧的渲染时间,那么就会重新调整一帧的渲染时间
    // 这里渲染频率最高不能超过120hz,不然渲染频率过高
    // 这里会自动的去调节帧的渲染频率,一开始的时候,我们默认是一秒33帧
    if (nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime) {
        if (nextFrameTime < 8) {
            nextFrameTime = 8;
        }
        activeFrameTime = nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
    } else {
        previousFrameTime = nextFrameTime;
    }
    // 当前帧的截止时间,用当前帧的开始时间加上每一帧的渲染时间
    frameDeadline = rafTime + activeFrameTime;
    // 通过消息通道发送消息
    if (!isMessageEventScheduled) {
        isMessageEventScheduled = true;
        port.postMessage(undefined);
    }
};

创建消息通道

// 通过MessageChannel来创建一个消息通道,这个消息通道有两个MessagePort类型的属性,port1和port2,就好比消息通道的两端,然后一端可以接收另一端发送的消息。
var channel = new MessageChannel();
// port2用来发送消息
var port = channel.port2;
// prot1是用来接收port2发送的消息,在这个回调中做具体的任务调度工作。
channel.port1.onmessage = function (event) {
    isMessageEventScheduled = false;

    var prevScheduledCallback = scheduledHostCallback;
    var prevTimeoutTime = timeoutTime;
    scheduledHostCallback = null;
    timeoutTime = -1;
    
    // 获取当前时间
    var currentTime = exports.unstable_now();
    
    // 这个用来表示当前帧已经过期,并且当前任务已经过期
    var didTimeout = false;
    // 当前帧已经过期
    if (frameDeadline - currentTime <= 0) {
        // 当前任务已经过期
        if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
            didTimeout = true;
        } else {
            // 当前任务没有过期,但是当前帧已经过期,那么就把当期任务放到下一帧执行
            // 这里可能是由于浏览器渲染比较久,导致当前帧过期了,那就将任务放到下一帧处理
            if (!isAnimationFrameScheduled) {
                isAnimationFrameScheduled = true;
                requestAnimationFrameWithTimeout(animationTick);
            }
            // 直接退出,不调用回调
            scheduledHostCallback = prevScheduledCallback;
            timeoutTime = prevTimeoutTime;
            return;
        }
    }
    // 当前帧没有过期,也就是说当前帧还有剩余时间,那么就执行任务
    if (prevScheduledCallback !== null) {
        isFlushingHostCallback = true;
        try {
            prevScheduledCallback(didTimeout);
        } finally {
            isFlushingHostCallback = false;
        }
    }
};
function flushWork(didTimeout) {
    if (enableSchedulerDebugging && isSchedulerPaused) {
        return;
    }
    // 表示正在执行任务
    isExecutingCallback = true;
    var previousDidTimeout = currentDidTimeout;
    currentDidTimeout = didTimeout;
    try {
        // 任务已经过期
        if (didTimeout) {
            while (firstCallbackNode !== null && !(enableSchedulerDebugging && isSchedulerPaused)) {
                // 获取当前时间
                var currentTime = exports.unstable_now();
                // 链表中的第一个任务的过期时间小于等于当前时间,说明第一个任务已经过期
                if (firstCallbackNode.expirationTime <= currentTime) {
                    do {
                        // 执行第一个任务,从链表中移除第一个任务,并把第二个任务作为链表的第一个任务
                        // 执行任务可能会产生新的任务,再把新的任务插入到任务链表中
                        flushFirstCallback();
                    } while (firstCallbackNode !== null &&             firstCallbackNode.expirationTime <= currentTime && !(enableSchedulerDebugging && isSchedulerPaused));
                    continue;
                }
                break;
            }
        } else {
            // 任务没有过期,并且当前帧还有剩余的时间,那么也会去执行任务
            if (firstCallbackNode !== null) {
                do {
                    if (enableSchedulerDebugging && isSchedulerPaused) {
                        break;
                    }
                    flushFirstCallback();
                } while (firstCallbackNode !== null && !shouldYieldToHost());
                // 这里的shouldYieldToHost()方法就是用来判断当前帧是否过期,取反的话就表示当前帧没有过期
            }
        }
    } finally {
        isExecutingCallback = false;
        currentDidTimeout = previousDidTimeout;
        // 最后,如果还有剩余任务的话,那么就再启动新的一轮任务执行调度
        if (firstCallbackNode !== null) {
            ensureHostCallbackIsScheduled();
        } else {
            isHostCallbackScheduled = false;
        }
        // 退出之前,如果还有任务,并且任务的优先级是最高级,那么就都执行一遍任务
        flushImmediateWork();
    }
}

根据上面代码,flushWork方法,会根据didTimeout有两种处理情况,如果didTimeout为true,就会把任务链表中的所有过期任务都执行一遍,如果didTimeout为false,那么会在当前帧过期之前尽可能多的去执行链表中的任务。

while (firstCallbackNode !== null && !(enableSchedulerDebugging && isSchedulerPaused)) {
    // 获取当前时间
    var currentTime = exports.unstable_now();
    // 链表中的第一个任务的过期时间小于等于当前时间,说明第一个任务已经过期
    if (firstCallbackNode.expirationTime <= currentTime) {
        do {
            // 执行第一个任务,从链表中移除第一个任务,并把第二个任务作为链表的第一个任务
            // 执行任务可能会产生新的任务,再把新的任务插入到任务链表中
            flushFirstCallback();
        } while (firstCallbackNode !== null &&             firstCallbackNode.expirationTime <= currentTime && !(enableSchedulerDebugging && isSchedulerPaused));
        continue;
    }
    break;
}

上面代码中,有两层循环,我们先来看里面这层循环,如果链表上有任务,并且任务已过期,那么就会执行这个任务,直到链表上没有任务或者链表上的任务没有过期为止,这个时候就会又执行外层的循环,外层循环中,又会重新获取一次当前时间,然后再去判断任务是否过期,如果过期,那么就继续执行内层循环,如果没有过期,那么就会推出外层循环,继续执行后面代码。

总结

react的任务调度流程:

  • 1、任务根据优先级和任务加入时的当前时间来确定任务的过期时间
  • 2、任务根据过期时间进行排序并添加到链表中。
  • 3、有两种情况会启动任务调度,一种情况是任务链表从无到有时,会启动任务调度,另外一种情况是新加入了最高优先级的任务,也会启动任务调度。
  • 4、任务调度是通过requestAnimationFrame和MessageChannel来模拟实现的。
  • 5、requestAnimationFrame的回调函数会在帧渲染前执行,用来计算当前帧的截止时间,MessageChannel的onmessage回调函数会在帧渲染后执行,根据当前帧截止时间,当前时间,任务链表中第一个任务的过期时间来决定当前帧是否执行任务(或者是到下一帧执行)
  • 6、如果执行任务,则根据任务是否过期来确定如何执行任务。任务过期的话就会把链表中所有过期的任务都执行一遍直到没有任务或者没有任务过期为止。任务没有过期的话,则会在当前帧过期之前尽可能多的执行任务。最后如果还有任务,就回到第5步,放到下一帧重新走流程。

Life of a frame

image

一帧里面除了上面图片中干的活之外就是空闲时间了。

image

react的setState机制

react的setState机制

在react应用中,如果我们想要改变组件的状态,只能通过调用setState方法来实现。而setState方法在react内部具体是怎么执行的呢?

看个例子

class App extends Component {
    constructor (props) {
        super(props);
        this.state = {
            count : 0
        }
    }
    add = () => {
        this.setState({
            count : this.state.count + 1
        });
        console.log(this.state.count);
    }
    componentDidMount () {
        this.setState({
            count : this.state.count + 1
        });
        console.log(this.state.count);
    }
    render () {
        return (
            <div>
                <div>{this.state.count}</div>
                <button onClick={this.add}>add</button>
            </div>
        )
    }
}

当页面加载完成后,在componentDidMount生命周期中和点击事件中分别都调用了setState方法,更新组件的状态,但是我们调用该方法后,立即去获取组件的最新状态,却是获取不到的,这是为什么呢?难道setState方法是异步执行的?

再看个例子

class App extends Component {
    constructor (props) {
        super(props);
        this.state = {
            count : 0
        }
    }
    componentDidMount () {
        setTimeout(() => {
            this.setState({
                count : this.state.count + 1
            });
            console.log(this.state.count);
        } , 0);
        document.addEventListener('click' , () => {
            this.setState({
                count : this.state.count + 1
            });
            console.log(this.state.count);
        })
    }
    render () {
        return (
            <div>
                <div>{this.state.count}</div>
            </div>
        )
    }
}

当页面加载完成后,在setTimeout定时器的回调里调用setState方法后,可以立即获取到最新的状态,并且在原生事件的回调中调用setState方法,也能立即获取到最新的状态,这又是为什么呢?

setState到底是不是异步的?

setState方法到底是不是异步的?这个问题应该是面试react的时候,被问到的概率比较大。我们可以从上面的例子中,一个一个的来看setState底层到底是怎么执行的。

1、react合成事件中的setState

从上面第一个例子中,我们可以得到:在react合成事件中调用setState方法,并不能立即得到最新的结果,所以setState方法是“异步”执行的。我们具体来了解下是如何“异步”执行的。

首先我们来看一下在合成事件中,当我们触发点击事件的执行过程是怎么样的。

image

2、react生命周期钩子函数中的setState

image

function requestWork(root, expirationTime) {
  addRootToSchedule(root, expirationTime);
  // 剩下的工作会被安排在当前这一批渲染的最后
  // 也就是说,等到所有的状态更新完之后,最后才进行统一的组件更新操作。
  if (isRendering) {
    return;
  }

  // 这里主要是针对react的合成事件触发时的回调函数中执行的更新操作
  // 在合成事件回调函数中更新所有状态之后,再统一执行组件的更新操作。
  if (isBatchingUpdates) {
    if (isUnbatchingUpdates) {
      nextFlushedRoot = root;
      nextFlushedExpirationTime = Sync;
      performWorkOnRoot(root, Sync, false);
    }
    return;
  }

  // 同步操作
  // 比如直接通过addEventListener绑定事件回调中调用setState方法更新状态
  // 比如setTimeout定时器回调函数中调用setState方法更新状态
  // 上面的两种情况,都会直接进行同步操作,也就是说,在调用setState方法后面,立即获取最新状态,是可以获取到的。
  if (expirationTime === Sync) {
    performSyncWork();
  } else {
    // 异步渲染会走这里
    scheduleCallbackWithExpirationTime(root, expirationTime);
  }
}

3、原生事件中的setState

如果我们在原生事件中调用setState方法,那么我们可以立即获取到最新的状态,比如:

import React, { Component } from 'react';
class App extends React.Component {
    constructor (props) {
        super(props);
        this.state = {
            val : 0
        }
    }
    componentDidMount() {
        document.addEventListener('click' , () => {
            this.setState({
                val : this.state.val + 1
            });
            // 每次打印的时候,都能打印最新的状态
            console.log(this.state.val);
        });
    }

    render() {
        return <div>{this.state.val}</div>
    }
}
export default App;

上面代码中,每次打印的都是最新的状态。通过上面所讲的方式,调用setState方法是获取不到最新的状态,怎么这里又可以获取呢?

image

原生事件的回调函数中调用setState方法,当执行到requestWork的时候,会执行expirationTime === Sync分支,它并没有被返回,而是继续执行performSyncWork方法,找到需要更新的数据,并进行组件的更新,等到所有都执行完之后,才会回到原生事件回调函数里,接着执行setState方法之后的代码,所有我们这里是可以立即获取到最新的状态。

定时器(setTimeout或setInterval)中的setState

定时器任务其实是在合成事件中,也可以在react生命周期函数中,或者在原生事件中,但是如果我们了解浏览拿的事件循环机制的话,就指定定时器任务是一个异步任务,所以不管是在合成事件的回调函数中,还是原生事件的回调函数中,还是react的生命周期函数中,当执行到具体代码时,会把异步任务放入到一个异步队列中,等到这一次事件循环结束之后,再拿出来执行。所以当执行到定时器任务的回调函数时,其实上一个事件循环已经结束了。那么无论如何它都能获取到最新的状态值。

比如,在合成事件中,调用setTimeout(() => {this.setState(...)} , 0),当合成事件回调函数执行到setTimeout时,会把定时器任务放入异步队列中,然后会执行finally语句块(看源码就知道了)里面的代码,等finally语句块里面代码执行完之后,isBatchingUpdates又变成了false,导致最后去执行异步队列里的setState方法时,requestWork方法里面的expirationTime === Sync为true,走的和原生事件中的setState一样的流程。所以在setState之后是可以获取到最新的状态值。

总结

  • setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。
  • setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
  • setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。

参考这篇

redux介绍

redux

redux是一种状态管理器,它不属于react,但是我们使用redux来管理react应用中的状态。

redux有三大原则:单一数据源,state是只读的,使用纯函数来修改state。

redux是一种状态管理器,如果我们想要改变状态,只能通过dispatch(action)的方式来改变。redux有三个重要的东西:action,reducer,store。

action

action是一个普通的对象,该对象会有一个type属性,其他属于自己可以自定义,action表示的是描述要发生什么事情,它不会去修改state,action是将数据传入到store的有效载荷。

{
    type : 'ADD_TODO',
    text : text
}

我们知道action是一个普通的对象,但是一般我们都是通过action creator函数来返回这个对象,比如这样:

const addTodo = (text) => {
    return {
        type : 'ADD_TODO',
        text
    }
}

reducer

指定了应用状态的变化如何响应 actions 并发送到 store 的。

import { VisibilityFilters , SET_VISIBILITY_FILTER , ADD_TODO , TOGGLE_TODO } from './actions';
const initialState = {
    visibilityFilter : VisibilityFilters.SHOW_ALL,
    todos : []
};
const { SHOW_ALL } = VisibilityFilters;

function visibilityFilter (state = SHOW_ALL , action) {
    switch (action.type) {
        case SET_VISIBILITY_FILTER :
        return action.filter;
        default : 
        return state;
    }
}

function todos (state = [] , action) {
    switch (action.type) {
        case ADD_TODO : 
        return [
            ...state ,
            {
                text : action.text,
                completed : false
            }
        ]
        case TOGGLE_TODO :
        return state.map((todo , index) => {
            if (index == action.index) {
                return Object.assign({} , todo , {
                    completed : !todo.completed
                })
            }
            return todo;
        });
        default :
        return state;
    }
}

function todoApp (state = initialState , action) {
    return {
        visibilityFilter : visibilityFilter(state.visibilityFilter , action),
        todos : todos(state.todos , action)
    }
}

export default todoApp;

store

store是一个普通的对象,是连接action和reducer的桥梁,当我们调用redux库中的createStore方法,将reducer作为参数传入,并返回的就是一个store对象,在redux应用中,store只能有一个,并且store内部管理着react应用的状态。通过调用store的dispatch(action)方法来描述要执行的操作,然后会调用reducer函数,执行相关的状态修改的操作,最后store会重新更新内部state。

const store = createStore(reducer);

数据流

redux是单向数据流

store.dispatch(action) -> reducer(state , action) -> state

调用store.dispatch(action)方法,然后执行传入的reducer函数,改变state,最后更新state。

redux如何处理异步数据

上面我们说的操作都是同步操作,没有涉及到异步数据的操作,如果是异步action,那么我们需要使用redux-thunk库,通过使用这个第三方库,我们的action创建函数不仅可以返回一个对象,而且还可以返回一个函数,而在返回的这个函数里,我们可以有副作用的操作,比如请求数据。

例子:

// actions.js
import fetch from 'cross-fetch';
export const REQUEST_POSTS = 'REQEUST_POSTS';
function requestPosts (subreddit) {
    return {
        type : REQUEST_POSTS,
        subreddit
    }
};

export const RECEIVE_POSTS = 'RECEIVE_POSTS';
function receivePosts (subreddit , data) {
    return {
        type : RECEIVE_POSTS,
        subreddit,
        data : data.data.children.map(child => child.data),
        receivedAt : Date.now()
    }
};

export const SELECTED_SUBREDDIT = 'SELECTED_SUBREDDIT';
export function selectSubreddit (subreddit) {
    return {
        type : SELECTED_SUBREDDIT,
        subreddit
    }
};

export const REQUEST_ERROR = 'REQUEST_ERROR';
function fectchError (subreddit , error) {
    return {
        type : REQUEST_ERROR,
        subreddit,
        error,
        receivedAt : Date.now()
    }
};

export function fetchData (subreddit) {
    return function (dispatch) {
        dispatch(requestPosts(subreddit));
        return fetch(`https://www.reddit.com/r/${subreddit}.json`)
        .then(res => res.json())
        .then(res => dispatch(receivePosts(subreddit , res)))
        .catch(err => dispatch(fectchError(subreddit , err)))
    }
};
// reducers.js
import { combineReducers } from 'redux';
import {
    REQUEST_POSTS,
    RECEIVE_POSTS,
    REQUEST_ERROR,
    SELECTED_SUBREDDIT
} from './actions';

function selectedSubreddit (state = 'reactjs' , action) {
    switch (action.type) {
        case SELECTED_SUBREDDIT :
        return action.subreddit;
        default :
        return state;
    }
};

function posts (state = {
    isFetching : false,
    error : false,
    items : []
} , action) {
    switch (action.type) {
        case REQUEST_POSTS : 
        return Object.assign({} , state , {
            isFetching : true
        });
        case RECEIVE_POSTS :
        return Object.assign({} , state , {
            isFetching : false,
            items : action.data,
            lastUpdated : action.receivedAt
        });
        case REQUEST_ERROR :
        return Object.assign({} , state , {
            isFetching : true,
            error : action.error
        });
        default : 
        return state;
    }
}

function postsBySubreddit (state = {} , action) {
    switch (action.type) {
        case REQUEST_POSTS:
        case RECEIVE_POSTS:
        case REQUEST_ERROR:
        return Object.assign({} , state , {
            [action.subreddit] : posts(state[action.subreddit] , action)
        });
        default :
        return state;
    }
}

const rootReducer = combineReducers({
    postsBySubreddit,
    selectedSubreddit
});
export default rootReducer;
// App.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { selectSubreddit , fetchData } from './actions';

class App extends Component {
    constructor (props) {
        super(props);
    }
    selectSubreddit = (e) => {
        const { dispatch } = this.props;
        dispatch(selectSubreddit(e.target.value));
    }
    componentDidMount () {
        const { dispatch } = this.props;
        dispatch(fetchData(this.props.selectedSubreddit));
    }
    componentWillReceiveProps (newProps) {
        if (newProps.selectedSubreddit !== this.props.selectedSubreddit) {
            const { dispatch , selectedSubreddit } = newProps;
            dispatch(fetchData(selectedSubreddit));
        }
    }
    render () {
        const { items , error } = this.props;
        return (
            <div style={{margin : '40px'}}>
                <span>请选择</span>
                <select onChange={this.selectSubreddit} name="" id="">
                    <option value="reactjs">reactjs</option>
                    <option value="vuejs">vuejs</option>
                </select>
                {
                    error ? <p>请求失败</p> :
                    <ul>
                        {
                            items.map((item , index) => (
                                <li key={index}>{item.title}</li>
                            ))
                        }
                    </ul>
                }
            </div>
        )
    }
}

const mapStateToProps = function (state) {
    const { postsBySubreddit , selectedSubreddit } = state;
    const {
        isFetching,
        lastUpdated,
        items,
        error
    } = postsBySubreddit[selectedSubreddit] || {
        isFetching : false,
        items : []
    };
    return {
        isFetching,
        lastUpdated,
        error,
        items,
        selectedSubreddit
    }
}
export default connect(mapStateToProps)(App);
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import thunkMiddleware from 'redux-thunk';
import { createStore , applyMiddleware } from 'redux';
import rootReducer from './reducers';
import App from './App';

let store = createStore(rootReducer , applyMiddleware(thunkMiddleware));
ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));

函数节流与防抖

函数防抖和节流

函数防抖和节流,主要应用于事件触发频繁的场景,比如说,滚动条事件,鼠标事件,窗口resize事件等,而我们通过使用函数防抖和节流来控制事件触发后的回调的频率,这个主要用于浏览器性能优化。

函数防抖的原理?

函数防抖的原理就是:你可以频繁的触发事件,但是回调函数的执行一定是在触发事件的n秒后才执行,如果在一个事件触发的n秒内又触发了一个事件,那么就会以新的事件的时间为准,n秒之后才执行回调。

// 函数防抖
function debounce (fn , time) {
	var timerId = null;
	return function () {
		var context = this;
		var args = arguments;
		clearTimeout(timerId);
		timerId = setTimeout(function () {
			fn.apply(context , args);
		} , time)
	}
};

防抖例子:

var box = document.getElementById('box');
var num = 0;
function handler (e) {
	num++;
	box.innerText = num;
};
function debounce (fn , time) {
	var timerId = null;
	return function () {
		var context = this;
		var args = arguments;
		clearTimeout(timerId);
		timerId = setTimeout(function () {
			fn.apply(context , args);
		} , time)
	}
};
box.addEventListener('mousemove' , debounce(handler , 2000));

函数节流的原理?

函数节流的原理:可以频繁的触发事件,但是我们可以控制执行触发事件的回调的频率,就像滴水一样,每隔n秒执行一次回调,如果在n秒之内触发事件的回调不会执行。有两种比较常用的方式:时间戳和定时器

1、使用时间戳的方式实现函数节流,其实就是一开始的时候创建一个时间戳,然后获取当前的时间戳,用当前的事件戳减去一开始的时间戳,如果大于n秒,那么就执行,并且将之前的时间戳更新为当前时间戳。

function throttle (fn , time) {
	var startTime = new Date();
	return function () {
		var endTime = new Date();
		var context = this;
		var args = arguments;
		if (endTime - startTime > time) {
			fn.apply(context , args);
			startTime = endTime;
		}
	}
};

这种函数节流的方式,当事件触发时会立刻执行回调,然后每隔n秒执行一次回调。

2、使用定时器的方式实现的函数节流,其实就是设置一个定时器,判断定时器是否存在,如果不存在,则每隔n秒执行一次回调,然后情况定时器,如果存在,则不执行回调。

function throttle (fn , time) {
	var timerId = null;
	return function () {
		var context = this;
		var args = arguments;
		if (!timerId) {
			timerId = setTimeout(function () {
				timerId = null;
				fn.apply(context , args);
			} , time);
		}
	};
};

这种函数节流的方式,当事件触发时不会立刻执行回调,而是要等过了n秒才执行一次,但是当我们停止触发事件的时候,过了n秒还是会再执行一次回调。

所以如果我们需要一开始的时候执行一次回调,然后在停止事件之后再执行一次回调,可以将两者组合在一起使用。

function throttle (fn , time) {
	var startTime = new Date();
	var timerId = null;
	return function () {
		var endTime = new Date();
		var context = this;
		var args = arguments;
		if (endTime - startTime > time) {
			if (!timerId) {
				startTime = endTime;
				fn.apply(context , args);
			}
			clearTimeout(timerId);
			timerId = null;
		} else if (!timerId) {
			timerId = setTimeout(function () {
				timerId = null;
				startTime = new Date();
				fn.apply(context , args);
			} , time)
		}
	}
};

Switch组件

react路由组件之Switch组件

组件表示的是只会渲染path与路由地址匹配的第一个子元素,并不会渲染所有相匹配的子元素。

class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Link to="/">home</Link>
                    <Link to="/about">about</Link>
                    <Link to="/user">user</Link>
                    <Route path="/" component={Home} />
                    <Route path="/about" component={About} />
                    <Route path="/user" component={User} />
                </div>
            </Router>
        )
    }
}

上面代码中,当我们导航到/about和/user页面时,始终会渲染"/"页面的组件。如果我们只想渲染路径相匹配的组件,那么就得给组件添加一个exact属性,这样表示路径要完全匹配才能渲染,效果看上去是达到了,但是其他的路由组件还是会去匹配,如果没有,那么就没有渲染组件,如果有匹配到,那么也会渲染出来。

class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Link to="/">home</Link>
                    <Link to="/">about</Link>
                    <Link to="/user">user</Link>
                    <Route path="/" exact component={Home} />
                    <Route path="/about" component={About} />
                    <Route path="/user" component={User} />
                </div>
            </Router>
        )
    }
}

其实我们只需要匹配到就可以了,不用再往下面进行匹配了。这个时候我们就要使用组件将所有的或者组件包裹起来,那么再匹配的时候,只要匹配到,就不会再往下匹配了。

const Home = () => <h1>home</h1>;
const About = () => <h1>about</h1>;
const User = () => <h1>user</h1>;
const NotMatch = () => <h1>Not Found the page!</h1>

class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Link to="/">home</Link>
                    <Link to="/about">about</Link>
                    <Link to="/user">user</Link>
                    <Switch>
                        <Route path="/" exact component={Home} />
                        <Route path="/about" component={About} />
                        <Route path="/user" component={User} />
                        <Route component={NotMatch} />
                    </Switch>
                </div>
            </Router>
        )
    }
}

Switch组件有以下几个属性

  • location属性
    • 该属性是一个对象,主要用于代替当前url地址去匹配子元素的path属性。
const Home = () => <h1>home</h1>;
const About = () => <h1>about</h1>;
const User = () => <h1>user</h1>;
const NotMatch = () => <h1>Not Found the page!</h1>
class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Link to="/">home</Link>
                    <Link to="/about">about</Link>
                    <Link to="/user">user</Link>
                    <Switch location={{
                        pathname : '/about'
                    }}>
                        <Route path="/" exact component={Home} />
                        <Route path="/about" component={About} />
                        <Route path="/user" component={User} />
                        <Route component={NotMatch} />
                    </Switch>
                </div>
            </Router>
        )
    }
}

上面代码中,不管里点击哪个链接,始终匹配的是"/about"路径,渲染About组件。因为Switch组件的location属性会匹配子元素的路径而不是url地址。

Switch组件的子元素只能是Route组件或者Redirect组件。并且只有第一个与url地址相匹配的子元素被渲染。Route组件是拿该组件的path属性与当前url地址去匹配,而Redirect组件是拿该组件的from属性与当前url地址去匹配,如果Route组件没有path属性或者Redirect组件没有from属性,那么会匹配任何路径,渲染该组件。

const Home = () => <h1>home</h1>;
const About = () => <h1>about</h1>;
const User = () => <h1>user</h1>;
const NotMatch = () => <h1>Not Found the page!</h1>

class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Link to="/">home</Link>
                    <Link to="/about">about</Link>
                    <Link to="/user">user</Link>
                    <Switch>
                        <Route path="/" exact component={Home} />
                        <Route path="/about" component={About} />
                        <Redirect from="/user" to="/about" />
                        <Route component={NotMatch} />
                    </Switch>
                </div>
            </Router>
        )
    }
}

如果Switch组件有一个location属性,那么这个属性会覆盖与它相匹配的子元素的location属性。

const Home = () => <h1>home</h1>;
const About = (props) => {
    console.log(props.location);
    return (
        <h1>about</h1>
    )
};
const User = () => <h1>user</h1>;
const NotMatch = () => <h1>Not Found the page!</h1>

class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Link to="/">home</Link>
                    <Link to="/about">about</Link>
                    <Link to="/user">user</Link>
                    <Switch location={{
                        pathname : '/about'
                    }}>
                        <Route path="/" exact component={Home} />
                        <Route path="/about" component={About} />
                        <Redirect from="/user" to="/about" />
                        <Route component={NotMatch} />
                    </Switch>
                </div>
            </Router>
        )
    }
}

当我们打印location属性时,结果为:

pathname: "/about"

子元素的location属性被覆盖掉了。

redux之减少样板代码

Redux之减少样板代码

我们在写react应用的时候,基本上都会使用到redux用来管理react应用的状态。那么在管理状态的过程中,我们会编写大量的action creator函数。但是action creator函数的模板样式其实都比较类似,那么有什么方法可以通过编写一个函数,来生成我们想要的action creator,这样我们就不需要单独写很多哥action creator函数了。

action creator生成器

其实我们可以编写一个函数,专门用来生成action creator。首先我们都知道,action creator函数都会返回一个对象,这个对象有一个type属性,然后还有其他属性。那么我们可以这样一步一步的来实现(假设这个函数名叫做actionCreatorFnMaker):

  • 首先,函数actionCreatorFnMaker会返回一个函数,返回的这个函数就是aciton creator
  • 其次,返回的这个函数会返回一个对象,这个对象有一个type字段和其他字段(比如需要更新的数据),所以我们要在创建action creator函数的时候把需要的action字段确定好
function actionCreatorFnMaker (type , ...args1) {
    return function (...args2) {
        let result = {type};
        args1.forEach((arg , index) => {
            result[args1[index]] = args2[index];
        });
        return result;
    }
};

我们来测试一下:

const ADD_TODO = 'ADD_TODO';
const DELETE_TODO = 'DELETE_TODO';
const addTodo = actionCreatorFnMaker(ADD_TODO , 'name');
const deleteTodo = actionCreatorFnMaker(DELETE_TODO , 'index');
console.log(addTodo('andy'))
console.log(deleteTodo(0));

结果为:

image

异步aciton creator

当我们在使用异步action的时候,一般都会这样写:

function asyncAction (...args) {
    return function (dispatch) {
        dispatch(startRequest());
        return fetch('xxx').then(res => {
            dispatch(requestSuccess({
                res
            }));
        })
    }
}

当我们需要通过请求来获取后端数据的时候,我们就会使用到中间件。如果我们想要编写自己的异步action creator函数,那么我们首先需要编写一个自定义的中间件函数,通过这个中间件来捕捉我们编写的异步action creator函数中的一些操作。

  • 编写中间件函数
let thunk = ({ dispatch , getState }) => next => action => {
    let {
        types,
        shouldCallApi = () => true,
        callApi,
        payload = {}
    } = action;
    // 如果没有types,那么就执行跳过这个中间件,执行下面的中间件
    if (!types) {
        return next(action);
    }
    if (!Array.isArray(types) || types.length !== 3 || !types.every(type => typeof type === 'string')) {
        throw new Error('Expected an array of three string types.');
    }
    if (typeof callApi !== 'function') {
        throw new Error('callApi must be function');
    }
    if (!shouldCallApi(getState())) {
        return;
    }
    let [startType , successType , failType] = types;
    dispatch(Object.assign({} , payload , {
        type : startType
    }));
    callApi().then(response => {
        return response.json();
    }).then(json => {
        dispatch(Object.assign({} , payload , {
            type : successType,
            posts : json.data.children.map(child => child.data)
        }))
    }).catch(err => {
        dispatch(Object.assign({} , payload , {
            type : failType,
            error : err.message
        }));
    })
}
  • 异步action creator函数
function postData (subreddit) {
    return {
        types : [REQUEST_START , REQUEST_SUCCESS , REQUEST_FAIL],
        shouldCallApi : state => {
            let posts = state.postsBySubreddit[subreddit];
            if (!posts) {
                return true;
            } else if (posts.isFetching) {
                return false;
            } else {
                return posts.didInvalidate;
            }
        },
        callApi : () => fetch(`https://www.reddit.com/r/${subreddit}.json`),
        payload : {subreddit}
    }
};

上面代码,当我们dispatch一个异步action,那么中间件会捕捉到这个异步action,然后对这个异步action进行处理。

Reducers函数 生成器

在写reducer函数的时候,我们发现我们会写很多的switch/case语句,来判断不同类型的action执行不同的操作。如果我不想写那么多的switch/case语句,那有没有其他方式可以解决这一的问题呢?答案肯定是有的,我们可以将每一个switch/case部分都写成一个函数,针对这一类型的action的操作都写在函数里。怎么做到这一点呢?我们需要建立一个action类型到action处理函数的映射对象。比如:

function createReducer (initialState , handles) {
    return function (state = initialState , action) {
        if (handles.hasOwnProperty(action.type)) {
            return handles[action.type](state , action);
        } else {
            return state;
        }
    }
};

上面代码就是一个自动创建reducer的函数代码,我们根据type类型和处理type类型的函数之间建立一个映射关系。当dispatch一个type类型的aciton,那么就会执行具体的函数。

const ADD_TODO = 'ADD_TODO';
const DELETE_TODO = 'DELETE_TODO';
const reducer = createReducer([] , {
    ADD_TODO : function (state , action) {
        let text = action.text;
        return [...state , text];
    },
    DELETE_TODO : function (state , action) {
        let index = action.index;
        return [
            ...state.slice(0 , index),
            ...state.slice(index + 1)
        ]
    }
});

测试代码:

function addTodo (text) {
    return {
        type : ADD_TODO,
        text
    }
};

function deleteTodo (index) {
    return {
        type : DELETE_TODO,
        index
    }
};

let store = createStore(todos);

store.subscribe(function () {
    console.log(store.getState());
})

store.dispatch(addTodo('andy'));
store.dispatch(addTodo('jack'));
store.dispatch(addTodo('tom'));
setTimeout(() => {
    store.dispatch(deleteTodo(1));
} , 2000)

Route组件

react路由组件之Route组件

Route组件可能是React路由中最重要的一个组件了,组件最基本的职责就是当地址匹配组件的path属性时,渲染相应的组件。

组件有三个属性可以渲染组件,component , render , children。不同的情况下,可以使用不同的方式,但是只能在组件中使用其中的一种方式来渲染,并且大部分情况下,我们都是使用component。

组件的三个渲染方式,都会传递同样的路由属性(history , location , match)。

Route组件有以下几个属性

  • component属性
    • 该属性是一个React组件,指的是当路由地址与组件的path相匹配时,会渲染的组件。同时会接收路由属性。
  • render属性
    • 该属性是一个函数,适用于内联渲染。当与路径向匹配的时候,就会调用该函数,渲染组件。
  • children属性
    • 该属性是一个函数,和render差不多,不过可以用来动态的展示组件差別之处在于,children会在路径不匹配的时候也调用回调从而渲染函数,而render只会在路径匹配的时候触发回调。
  • path属性
    • 该属性可以是一个字符串,也可以是一个数组,表示url地址与path相匹配的时候,就会渲染组件。如果一个组件没有path属性,那么该组件将匹配任何地址。
  • exact属性
    • 该属性是一个布尔值,表示跳转地址与Route组件的path属性完全匹配时才渲染组件。比如:当exact为true时,"/one"是不能和"/one/two"匹配的,如果是false的话,就可以。
  • strict属性
    • 该属性是一个布尔值,表示不会匹配路径的最后一个斜杠,比如:'/one'和'/one/'是不匹配的。
  • sensitive属性
    • 该属性是一个布尔值,表示匹配路径和path不区分大小写,比如:"/one"和"/One"是相匹配的。
import React, { Component } from 'react';
import { BrowserRouter as Router , Route , Switch , Link , withRouter , Redirect } from 'react-router-dom';

const Home = (props) => {
    return (
        <h1>Home</h1>
    )
};
const About = (props) => {
    return (
        <h1>About</h1>
    )
};
const User = (props) => {
    return (
        <h1>User</h1>
    )
};
class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Link to="/">home</Link>
                    <Link to="/about">about</Link>
                    <Link to="/user">user</Link>
                    <Route path="/" exact component={Home} />
                    <Route path="/about" component={() => {
                        return (
                            <div>about</div>
                        )
                    }} />
                    <Route path="/user" component={User} />
                </div>
            </Router>
        )
    }
}
export default App;
// 当一个Route组件没有path属性时,那么表示跳转到任何路由都会渲染该组件。
class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Link to="/">home</Link>
                    <Link to="/about">about</Link>
                    <Link to="/user">user</Link>
                    <Route component={User} />
                </div>
            </Router>
        )
    }
}

上面代码中,当点击三个链接,都会渲染User组件。

// path属性是数组的时候
const Home = () => <h1>home</h1>;
const About = () => <h1>about</h1>;
const User = () => <h1>user</h1>;

class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <Link to="/">home</Link>
                    <Link to="/about">about</Link>
                    <Link to="/user/one">user</Link>
                    <Link to="/profile/one">profile</Link>
                    <Switch>
                        <Route path="/" exact component={Home} />
                        <Route path="/about" component={About} />
                        <Route path={['/user/:id' , '/profile/:id']} component={User} />
                    </Switch>
                </div>
            </Router>
        )
    }
}

上面代码中,点击user和profile链接其实都是会渲染User组件。

Prompt组件

react路由组件之Prompt组件

Prompt组件主要用于在用户离开页面之前提醒用户。当

Prompt组件有以下几个属性:

  • when属性
    • 该属性是一个布尔值,表示的是是否需要渲染Prompt组件。
  • message属性
    • 该属性可以是一个字符串或者是一个函数,如果是一个字符串,那么就是弹框提示的文字,如果是一个函数,那么必须返回一个字符串(弹框提示问题)或者返回一个true(没有弹框,会直接跳转到你导航的页面)
class Form extends Component {
    constructor (props) {
        super(props);
        this.state = {
            isBlocking : false
        }
    }
    render () {
        const {isBlocking} = this.state;
        return (
            <form onSubmit={event => {
                event.preventDefault();
                event.target.reset();
                this.setState({
                    isBlocking : false
                })
            }}>
                <Prompt when={isBlocking} message={location => {
                    return `Are you sure you want to go to ${location.pathname}`;
                }} />
                <p>Blocking? {isBlocking ? "yes , click a link ore the back button" : 'nope'}</p>
                <div>
                    <input type="text" placeholder="type something to block transitions" onChange={(event) => {
                        let value = event.target.value;
                        this.setState({
                            isBlocking : value.length > 0
                        });
                    }} />
                </div>
                <button>Submit to stop blocking</button>
            </form>
        )
    }
}

class App extends Component {
    render () {
        return (
            <Router>
                <div>
                    <ul>
                        <li>
                            <Link to="/">Form</Link>
                        </li>
                        <li>
                            <Link to="/one">One</Link>
                        </li>
                        <li>
                            <Link to="two">Two</Link>
                        </li>
                    </ul>
                    <Route path="/" exact component={Form} />
                    <Route path="/one" render={() => <h1>one</h1>} />
                    <Route path="/two" render={() => <h1>two</h1>} />
                </div>
            </Router>
        )
    }
}

上面代码就是一个简单的Prompt组件使用的例子,当我们在输入框输入东西时,然后点击链接,就会渲染Prompt组件,如果点击确定,那么会跳转,如果点击取消那么就不会跳转。

Ajax

ajax

ajax指的就是异步的javascript和XML,不过在使用ajax的过程中,我们很少会使用XML来进行数据的传递。ajax的出现,让我们可以在不刷新网页的情况下,发送http请求,与后端进行交互。

ajax的使用是比较简单的,基本上我们需要了解它是怎么发送请求的,以及它是怎么处理服务器响应的。

ajax怎么发送请求?

创建一个XMLHttpRequest对象,然后调用对象的open方法和send方法就可以了,我们来看一下具体怎么操作

// 创建XMLHttpRequest对象
var xhr = new XMLHttpRequest();

// 调用对象的open方法,来创建于服务器的连接
xhr.open(“GET” , “demo.txt” , true);

// 调用对象的send方法,来发送http请求
xhr.send(null);

ajax怎么处理服务器响应?

当我们发送ajax请求后,我们可以给xhr对象的onreadystatechange属性指定一个函数,每当请求的状态发生变化时,就会执行这个函数。

var xhr = new XMLHttpRequest();
xhr.open('get' , 'demo.txt');
// 每当请求状态方法变化的时候,就会执行fn
xhr.onreadystatechange = fn;
xhr.send(null);

那么ajax请求的状态有哪些呢?

状态值 说明
0 请求未被初始化
1 已经与服务器建立连接
2 服务器已经接受到请求
3 服务器正在处理请求
4 服务器已经处理完请求并准备响应

我们可以通过执行下面代码来打印出请求状态值

var xhr = new XMLHttpRequest();
xhr.open('get' , 'demo.txt');
xhr.onreadystatechange = function () {
    console.log(xhr.readyState);
};
xhr.send(null);

当readyState为4,并且status为200的时候,我们就可以通过responseText获取到响应的数据了。

var btn = document.getElementById('btn');
btn.addEventListener('click' , function () {
    var xhr = new XMLHttpRequest();
    xhr.open('get' , 'demo.txt');
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
            if (xhr.status == 200) {
                console.log(xhr.responseText);
            } else {
                console.log('请求失败');
            }
        }
    };
    xhr.send(null);
});

上面的代码是get请求,如果是post请求呢?我们需要传递一些数据到后端,让后端根据我们传递的数据做相应的处理,这个时候,我们需要设置请求头信息,比如,要设置Content-Type的头信息,该头信息一般我们可以设置三种类型

  • application/x-www-form-urlencoded,数据以表单提交的方式发送到后端
  • application/json,表示数据是以json格式发送到后端
  • multipart/form-data,表示数据是以FormData的方式发送到后端,一般用于浏览器需要向服务器发送多种数据类型的情况,比如一个请求即有字符串数据,又有文件类型的数据。

XMLHttpRequest对象有哪些常用属性和方法?

常用属性:

属性名 说明
onreadystatechange 当请求状态发生变化时会调用
readyState 请求状态码
responseText 请求的响应内容,是一个字符串
response 响应内容,不过具体类型有responseType的值来决定
responseType 响应类型
status 响应状态码
statusText 响应状态码对应的说明
responseXML XML文档
responseURL 响应的URL
timeout 请求的超时时长
upload 请求的上传过程

常用方法

方法名 说明
abort 如果请求已经发送,则立即中止请求
getAllResponseHeaders 获取所有的响应头信息
getResponseHeader 获取指定的响应头信息
open 初始化一个请求
send 发送一个请求
overrideMimeType 重写由服务器返回的MIME type
setRequestHeader 设置请求头信息,必须在open之后,send之前调用

监测进度

当通过ajax请求去获取服务器上的资源时,可以通过给xhr对象添加一些监听事件来监测资源的下载进度。

事件名 说明
loadstart 接收到响应数据的第一个字节时触发该事件
progress 接收到响应数据期间,持续不断的触发该事件
error 数据在加载期间,如果加载失败就会触发该事件,比如,突然断网
load 响应数据全部接收到时,会触发该事件
abort 当调用xhr.abort方法中止时触发该事件
loadend 通信完成时会触发该事件
// 点击按钮,去获取index.css文件内容。
var btn = document.getElementById('btn');
btn.addEventListener('click' , function () {
    var xhr = new XMLHttpRequest();
    xhr.addEventListener('loadstart' , function () {
        console.log('loadstart');
    });
    xhr.addEventListener('progress' , function () {
        console.log('progress');
    });
    xhr.addEventListener('load' , function () {
        console.log('load');
    });
    xhr.addEventListener('error' , function () {
        console.log('error');
    });
    xhr.addEventListener('abort' , function () {
        console.log('abort');
    });
    xhr.addEventListener('loadend' , function () {
        console.log('loadend');
    });
    xhr.open('get' , 'index.css');
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
            if (xhr.status == 200) {
                console.log(xhr.response);
            }
        }
    }
    xhr.send(null);
});

为了能够更好的看到事件被触发过程,我们可以在浏览器上把网速改低一点,正常情况下,触发事件的过程是这样的:

  • 1、触发loadstart事件,表示浏览器刚刚接收到服务器响应的数据
  • 2、触发progress事件,表示浏览器正在持续不断的接收服务器响应的数据,所以这个事件会持续不断的被触发,直到响应数据被浏览器全部接收或者出现网络异常,又或者请求被中止时,才会不触发。
  • 3、触发load事件,表示浏览器已经接收到全部的响应数据。
  • 4、触发loadend事件,表示浏览器和服务器的通信已经完成。

如果在接收响应数据期间,突然网络异常,那么会是怎么触发事件的呢?

  • 1、触发loadstart事件,表示浏览器刚刚接收到服务器响应的数据
  • 2、触发progress事件,表示浏览器正在持续不断的接收服务器响应的数据,所以这个事件会持续不断的被触发,直到响应数据被浏览器全部接收或者出现网络异常,又或者请求被中止时,才会不触发。
  • 3、触发error事件,表示当网络异常时(断网),浏览器无法接收到响应数据
  • 4、触发loadend事件,表示浏览器和服务器的通信已经完成。

如果在接收响应数据期间,人为调用abort方法中止请求,那么事件又是怎么触发的呢?

  • 1、触发loadstart事件,同上
  • 2、触发progress事件,同上
  • 3、触发abort事件,表示调用abort方法来中止请求
  • 4、触发loadend事件,同上
var btn = document.getElementById('btn');
btn.addEventListener('click' , function () {
    var xhr = new XMLHttpRequest();
    xhr.addEventListener('loadstart' , function () {
        console.log('loadstart');
    });
    xhr.addEventListener('progress' , function (evt) {
        if (evt.lengthComputable) {
            var percent = evt.loaded / evt.total;
            percent = percent.toFixed(2) * 100;
            console.log(percent + '%');
        }
    });
    xhr.addEventListener('load' , function (evt) {
        console.log('load');
    });
    xhr.addEventListener('error' , function () {
        console.log('error');
    });
    xhr.addEventListener('abort' , function () {
        console.log('abort');
    });
    xhr.addEventListener('loadend' , function () {
        console.log('loadend');
    });
    xhr.open('get' , 'index.css');
    xhr.send(null);
});

上面的代码,我们可以清楚的看到接受响应数据的进度,结果如下

image

我们这里看到的是下载的进度事件,除了下载之外,还有上传的进度事件,上传的相关进度事件是在ajax对象的upload属性上触发的。

函数的call和apply方法

call方法

call方法调用一个函数,其具有一个指定的this值和指定的参数列表。

fun.call(thisArg, arg1, arg2, ...)

thisArg参数,表示的是在fun函数运行时指定的this值。这里需要注意的是,如果是在非严格模式下,指定null或者undefined为this值时,this会自动执行全局对象(window),同时值为原始值的this会执行该原始值的包装对象。

// 非严格模式下
var a = 1;
function foo () {
    console.log(this.a);
}
foo.call(null);
// 打印结果为:1
// 严格模式下
'use strict';
var a = 1;
function foo () {
    console.log(this.a);
}
foo.call(null);
// 报错

如果在非严格模式下,函数调用call方法中没有传入第一个参数,那么会自动执行全局对象(window),如果在严格模式下,那么this值会是undefined。

// 非严格模式下
var a = 1;
function foo () {
    console.log(this.a);
}
foo.call();
// 严格模式下
'use strict';
var a = 1;
function foo () {
    // 这里的this值是undefined
    console.log(this.a);
}
foo.call();
// 报错

用法

使用call方法调用父构造函数

当我们需要继承父构造函数时,我们可以通过在子构造函数里调用call方法来执行父构造函数,从而实现继承父构造函数的属性。

function Person (name , age) {
    this.name = name;
    this.age = age;
}

function Andy (name , age) {
    Person.call(this , name , age);
}

function Jack (name , age) {
    Person.call(this , name , age);
}

var andy = new Andy('andy' , 22);
var jack = new Jack('jack' , 23);

使用call方法来绑定this值

var name = 'jack';

function say () {
    console.log(this.name);
}

var obj = {
    name : 'andy'
}
say.call(obj);

要注意,如果上面代码中不传入obj这个对象,那么在非严格模式下this值会执行全局对象(window),在严格模式下this值会是undefined。

模拟call方法

  • 1、绑定this。
  • 2、可以传入参数。
  • 3、当第一个参数不传入时,或者传入的是null或undefined,在非严格模式下会指向全局对象(window)。
  • 4、调用call方法可以有返回值。

因为所有的函数都有call方法,那么我们可以直接在函数的原型上添加该方法,首先我们要做的是调用call方法来绑定this值,当函数调用call方法时,会执行这个函数,并给这个函数指定一个this值。

Function.prototype.call2 = function (context) {
    context.fn = this;
    context.fn();
    // 当调用完之后,就可以删除对象上的这个属性
    delete context.fn;
};
Function.prototype.call2 = function (context) {
    context.fn = this;
    context.fn();
    delete context.fn;
};
var a = 1;
var obj = {
    a : 2
}
function foo () {
    console.log(this.a);
}
foo.call2(obj);
// 打印的结果为:2

但是当我们不传入第一个参数的时候或者传入的是null或者undefined,在非严格模式下this值会指向window

Function.prototype.call2 = function (context) {
    context = context || window;
    context.fn = this;
    context.fn();
    delete context.fn;
};

除了绑定this值,还可以传入其他参数到执行的函数里面,一开始我想到的是使用es6语法的rest参数方式,但是忽然觉得如果要模拟怎么能用es6语法呢?可以参考其他人的方式使用eval()来实现

Function.prototype.call2 = function (context) {
    context = context || window;
    var args = [];
    // 这里我们需要获取除第一个参数之外的其他参数
    for (var i = 1; i < arguments.length ; i++) {
        args.push('arguments[' + i + ']');
    };
    
    context.fn = this;
    eval('context.fn('+ args +')');
    delete context.fn;
};
Function.prototype.call2 = function (context) {
    context = context || window;
    var args = [];
    for (var i = 1; i < arguments.length ; i++) {
        args.push('arguments[' + i + ']');
    };
    
    context.fn = this;
    // args是一个数组,这里主要用到了字符串与数组相加的隐式转换,数组会调用toString()方法转为字符串,如果是使用数组的join()方法来将数组转为字符串,会报错。
    eval('context.fn('+ args +')');
    delete context.fn;
};
var a = 1;
var obj = {
    a : 2
}
function foo (name , age) {
    console.log(this.a);
    console.log(name);
    console.log(age);
}
foo.call2(obj , 'andy' , 22);

除了上面讲的之外,调用call方法还可以有返回值,而返回值就是执行函数的返回值

Function.prototype.call2 = function (context) {
    context = context || window;
    var args = [];
    for (var i = 1; i < arguments.length ; i++) {
        args.push('arguments[' + i + ']');
    };
    
    context.fn = this;
    // 将结果保存并返回即可
    var res = eval('context.fn('+ args +')');
    delete context.fn;
    return res;
};

apply和call方法的区别

其实apply和call方法的主要区别就是传入的参数不同,call方法传入的是一个参数列表,而apply方法传入的是一个保存参数列表的数组。

apply方法模拟

Function.prototype.apply2 = function (context , arr) {
    var context = context || window;
    var args = [];
    var res = '';
    context.fn = this;
    if (!arr) {
        res = context.fn();
    } else {
        for (var i = 0 ; i < arr.length ; i++) {
            args.push('arr['+i+']');
        };
        res = eval('context.fn('+ args +')');
    }
    delete context.fn;
    return res;
}

参考冴羽的文章,但是自己应该要按照前辈们的思路再去实现一遍,来巩固自己的基础知识。

React状态传递

react状态传递

react状态传递,其实说的就是react组件间是如何通信的。在说react组件间通信之前,我们应该要了解react组件之间有哪几种关系:父子组件,兄弟组件。

父组件向子组件通信

父组件可以通过向子组件传递==props==的方式来通信,通信是单向的。

class Parent extends Component {
    constructor (props) {
        super(props);
        this.state = {
            message : 'hello child'
        }
    }
    render () {
        return (
            <Child message={this.state.message} />
        )
    }
};

class Child extends Component {
    constructor (props) {
        super(props);
    }
    render () {
        return (
            <div>{this.props.message}</div>
        )
    }
}

如果组件层次比较深的话,我们可以通过{...props}的方式将父组件的状态一层层的传递给子组件。当父组件的state和props改变时,会导致所有子组件的声明周期改变。

class Parent extends Component {
    constructor (props) {
        super(props);
        this.state = {
            message : 'hello child'
        }
    }
    componentDidUpdate () {
        console.log('Parent updated');
    }
    componentDidMount () {
        setTimeout(() => {
            this.setState({
                message : 'hello world'
            })
        } , 1000)
    }
    render () {
        return (
            <Child1 message={this.state.message} />
        )
    }
};

class Child1 extends Component {
    constructor (props) {
        super(props);
    }
    componentDidUpdate () {
        console.log('Child1 updated');
    }
    render () {
        return (
            <div>
                <div>{this.props.message}</div>
                <Child1_1 {...this.props} />
            </div>
        )
    }
}

class Child1_1 extends Component {
    constructor (props) {
        super(props);
    }
    componentDidUpdate () {
        console.log('Child1_1 updated');
    }
    render () {
        return (
            <div>{this.props.message}</div>
        )
    }
}

子组件向父组件通信

react组件的通信是单向的,只能是从上到下的方式来通信,如果子组件向父组件通信,其实也是通过父组件向子组件传递props的方式进行,只是父组件向子组件传递的是函数,在子组件中调用这个函数,并将数据作为参数传入到这个函数中,从而实现子组件向父组件通信。

class Parent extends Component {
    constructor (props) {
        super(props);
        this.state = {
            message : 'hello child'
        }
    }
    componentDidUpdate () {
        console.log('Parent updated');
    }
    change (msg) {
        this.setState({
            message : msg
        })
    }
    render () {
        return (
            <Child1 change={(msg) => this.change(msg)} message={this.state.message} />
        )
    }
};

class Child1 extends Component {
    constructor (props) {
        super(props);
    }
    componentDidUpdate () {
        console.log('Child1 updated');
    }
    componentDidMount () {
        setTimeout(() => {
            // 调用函数,并将数据传递给父组件的函数
            this.props.change('hello andy');
        } , 2000)
    }
    render () {
        return (
            <div>
                <div>{this.props.message}</div>
                <Child1_1 {...this.props} />
            </div>
        )
    }
}

class Child1_1 extends Component {
    constructor (props) {
        super(props);
    }
    componentDidUpdate () {
        console.log('Child1_1 updated');
    }
    render () {
        return (
            <div>{this.props.message}</div>
        )
    }
}

如果组件层次结构太深的话,通过props来进行组件间的状态传递也是非常难以维护的。

兄弟组件之间的通信

兄弟组件之间的关系就是组件都拥有共同的父组件,如果想要一个组件的状态传递到另一个组件,我们可以先将一个组件的状态传递到父组件,再由父组件将状态通过props传递给另一个组件。

class Parent extends Component {
    constructor (props) {
        super(props);
        this.state = {
            message : 'hello child'
        }
    }
    change (msg) {
        this.setState({
            message : msg
        });
    }
    componentDidUpdate () {
        console.log('Parent updated!');
    }
    render () {
        return (
            <div>
                <Child1 change={(msg) => this.change(msg)} />
                <Child2 {...this.state} />
            </div>
        )
    }
};

class Child1 extends Component {
    constructor (props) {
        super(props);
        this.handleClick1 = this.handleClick1.bind(this);
    }
    handleClick1 () {
        this.props.change('我点击了child1 button');
    }
    componentDidUpdate () {
        console.log('Child1 updated!');
    }
    render () {
        return (
            <div>
                <p>child1</p>
                <button onClick={this.handleClick1}>child1 button</button>
            </div>
        )
    }
};

class Child2 extends Component {
    constructor (props) {
        super(props);
    }
    componentDidUpdate () {
        console.log('Child2 updated!');
    }
    render () {
        return (
            <div>
                <p>child2</p>
                <div>{this.props.message}</div>
            </div>
        )
    }
}

如果组件层级太深,兄弟组件之间通信就会变得比较漫长,首先让一个组件将状态一层一层的向上传递到共同的父组件上,然后由父组件又一层一层的向下传递给另一个组件,这样的方式其实是很容易出错,而且只要父组件的状态改变,都会引起所有子组件的生命周期改变。那有没有其他方式可以解决这个问题呢?我们可以使用发布订阅模式来解决这个问题。

发布订阅模式

我们可以在全局设计一个发布订阅模式,用来发布和订阅消息,只要一个组件订阅了这个消息,当另一个组件发布这条消息的时候,它就会接收到这个消息,从而执行相应的操作。

// 一个简单的发布订阅模式
let eventProxy = {
    events : {},
    on : function (key , fn) {
        if (this.events[key] == undefined) {
            this.events[key] = [];
        }
        this.events[key].push(fn);
    },
    trigger : function (key , ...args) {
        if (this.events[key].length > 0) {
            for (var i = 0 ; i < this.events[key].length ; i++) {
                this.events[key][i].apply(this , args);
            }
        };
    },
    off : function (key) {
        this.events[key] = [];
    }
};

class Parent extends Component {
    constructor (props) {
        super(props);
        this.state = {
            message : 'hello child'
        }
    }
    componentDidUpdate () {
        console.log('Parent updated!');
    }
    render () {
        return (
            <div>
                <Child1 />
                <Child2 {...this.state} />
            </div>
        )
    }
};

class Child1 extends Component {
    constructor (props) {
        super(props);
        this.handleClick1 = this.handleClick1.bind(this);
    }
    handleClick1 () {
        // 当点击Child1组件的按钮时,我们发布一个消息
        eventProxy.trigger('message' , '这是兄弟组件Child1发布的一个消息');
    }
    componentDidUpdate () {
        console.log('Child1 updated!');
    }
    render () {
        return (
            <div>
                <p>child1</p>
                <button onClick={this.handleClick1}>child1 button</button>
            </div>
        )
    }
};

class Child2 extends Component {
    constructor (props) {
        super(props);
        this.state = {
            message : '没有接收到兄弟组件Child1的消息'
        };
    }
    componentDidUpdate () {
        console.log('Child2 updated!');
    }
    componentDidMount () {
        // 这里来订阅这个消息
        eventProxy.on('message' , (message) => {
            this.setState({
                message
            })
        });
    }
    render () {
        return (
            <div>
                <p>child2</p>
                <div>{this.state.message}</div>
            </div>
        )
    }
}

Child2组件订阅了message,当我们点击Child1组件的按钮,发布一条message时,Child2就会执行相应的操作,这样避免了兄弟组件之间的状态要通过父组件作为中间人来传递,而且因为不会通过父组件来传递状态,所以并不会引发父组件及其所属子组件的生命周期改变。

redux

除了上面这几种方式来进行组件间通信之外,我们也可以使用redux。redux是将状态都保存在它的内部的state上面,通过调用dispatch方法来发送一个action,然后会调用reducer函数来修改state,通过subscribe方法来监听state的改变,如果state改变,就可以触发相应的回调函数,从而通过调用getState方法来获取最新的state。其实redux和发布订阅模式类似。

let reducer = function (state = 0 , action) {
    switch (action.type) {
        case 'add' :
        return state + 1;
        case 'decrease' :
        return state - 1;
        default :
        return state
    }
};

let store = createStore(reducer);

class Parent extends Component {
    constructor (props) {
        super(props);
        this.state = {
            tick : 0
        }
    }
    componentDidMount () {
        store.subscribe(() => {
            let tick = store.getState();
            this.setState({
                tick
            })
        })
    }
    render () {
        return (
            <div>
                <div>tick is {this.state.tick}</div>
                <Child1 tick={this.state.tick} />
            </div>
        )
    }
};

class Child1 extends Component {
    constructor (props) {
        super(props);
        this.add = this.add.bind(this);
        this.decrease = this.decrease.bind(this);
    }
    add () {
        store.dispatch({
            type : 'add',
            tick : this.props.tick + 1
        });
    }
    decrease () {
        store.dispatch({
            type : 'decrease',
            tick : this.props.tick - 1
        })
    }
    render () {
        return (
            <div>
                <p>Child1</p>
                <button onClick={this.add}>add</button>
                <button onClick={this.decrease}>decrease</button>
            </div>
        )
    }
};

Context

let themes = {
    light : {
        foreground : '#fff',
        background : '#222'
    },
    dark : {
        foreground : '#000',
        background : '#eee'
    }
};

let ThemeContext = React.createContext(themes.dark);

function ThemedButton (props) {
    return (
        <ThemeContext.Consumer>
            {theme => (
                <button {...props} style={{backgroundColor : theme.background}}>{props.children}</button>
            )}
        </ThemeContext.Consumer>
    )
};

function Toolbar (props) {
    return (
        <ThemedButton onClick={props.changeTheme}>change theme</ThemedButton>
    )
};

class Parent extends Component {
    constructor (props) {
        super(props);
        this.state = {
            theme : themes.light
        };
        this.toggleTheme = this.toggleTheme.bind(this);
    }
    toggleTheme () {
        this.setState({
            theme : this.state.theme === themes.dark ? themes.light : themes.dark
        });
    }
    render () {
        return (
            <div>
                <ThemeContext.Provider value={this.state.theme}>
                    <Toolbar changeTheme={this.toggleTheme} />
                </ThemeContext.Provider>
            </div>
        )
    }
}

前端模板实现

前端模板

前端模板引擎有很多,大部分都是大同小异,我们经常看到通过字符串来拼接很长的一端html代码,里面有html代码,有js代码。这样完全将html和js代码耦合在一起。

前端模板引擎原理:

通过前端模板函数将模板中的html代码和js语句、变量分离,然后通过Function构造函数来动态生成具有数据性的html代码。

具体代码实现:

function template (tpl , data) {
	var result = 'var arr = [];';
    var splitSymbol = '|';
	var str = tpl
		.replace(/(<[^%]+>)/g , "arr.push('$1')" + splitSymbol)
		.replace(/<%?=(\s*.+)%>/g , "arr.push($1)" + splitSymbol)
                .replace(/<%(\s*.+)%>/g , '$1' + splitSymbol)
	result = result + str;
	result  += 'return arr.join("")';
	result = result.split('|');
	result = result.map(function (item) {
		return item.replace(/\n/g , '');
	});
    return new Function('data' , result.join('\n'))(data);
};

上面的模板引擎函数,我们最后使用的是

new Function('data' , result.join('\n'))(data);

这样我们的字符串模板里面必须使用的是data对象,而不能使用其他名称,当然我们也可以稍微的改造一下,通过调用函数的apply方法来改变this的指向。

var fn = new Function('data' , result.join('\n'));
return fn.apply(data);

字符串模板是这样的:

<script type="text/html" id="tpl">
	<% if (this.show) { %>
		<span><%= this.name %></span>
	<% } else { %>
		<span><%= this.age %></span>
    <% } %>
    <% for (var i = 0 ; i < this.list.length ; i++) { %>
        <% if (this.list[i] >= 2) { %>
			<span><%= this.list[i] %></span>
			<div>sdfdsfdgdsg</div>
			<p>sdf</p>
        <% } else { %>
            <div style="color:#f00;"><%= this.list[i] %></div>
        <% } %>
	<% } %>
	<span>asdlfjas</span>
</script>

对于JavaScript语句,我们将语句放在"<% javascript语句 %>"中,而对于JavaScript值,我们放在"<%= 值 %>"中。

例子:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Document</title>
</head>
<body>
<div id="box"></div>
<script type="text/html" id="tpl">
	<% if (this.show) { %>
		<span><%= this.name %></span>
	<% } else { %>
		<span><%= this.age %></span>
    <% } %>
    <% for (var i = 0 ; i < this.list.length ; i++) { %>
        <% if (this.list[i] >= 2) { %>
			<span><%= this.list[i] %></span>
			<div>sdfdsfdgdsg</div>
			<p>sdf</p>
        <% } else { %>
            <div style="color:#f00;"><%= this.list[i] %></div>
        <% } %>
	<% } %>
	<span>asdlfjas</span>
</script>
<script>
var str = document.getElementById('tpl').innerHTML;
var obj = {
	show : false,
	name : 'andy',
	age : 22,
	address : '广州1',
    list : [1,2,3,4]
};
function template (tpl , data) {
	var result = 'var arr = [];';
    var splitSymbol = '|';
	var str = tpl
		.replace(/(<[^%]+>)/g , "arr.push('$1')" + splitSymbol)
		.replace(/<%?=(\s*.+)%>/g , "arr.push($1)" + splitSymbol)
                .replace(/<%(\s*.+)%>/g , '$1' + splitSymbol)
	result = result + str;
	result  += 'return arr.join("")';
	result = result.split('|');
	result = result.map(function (item) {
		return item.replace(/\n/g , '');
	});
	var fn = new Function('data' , result.join('\n'));
	return fn.apply(data);
};
var s = template(str , obj);
var box = document.getElementById('box');
box.innerHTML = s;
</script>
</body>
</html>

注意:

1、html代码注释的头部和尾部只能在一行当中,如果不在一行,那么模板渲染会出错。

3、html模板中的如果需要使用引号(比如写html标签属性),那么必须是双引号,不然会报错。

React组件生命周期

react组件生命周期(16以上版本)

React组件的生命周期可以分为三个过程:

  • 挂载过程(Mount):第一次把组件挂载到DOM树上。
  • 更新过程(Update):组件渲染更新的过程。
  • 卸载过程(Unmount):组件从DOM树删除的过程。

挂载过程

1、首先调用constructor,进行组件初始化操作,一般会在这里进行组件state初始化,以及绑定成员函数的this(这里我们也可以直接使用箭头函数,这样就不需要绑定在constructor里面绑定this了)

class App extends Component {
	constructor (props) {
		super(props);
		// 这里进行组件state初始化操作
		this.state = {
			name : 'andy'
		}
		// 绑定成员函数this
		this.handleClick = this.handleClick.bind(this);
	}
	handleClick () {
		this.setState({
			name : 'jack'
		});
	}
	render () {
		const { name } = this.state;
		return (
			<div>
				<button onClick={this.handleClick}>click here</button>
				<div>{name}</div>
			</div>
		)
	}
}

2、调用componentWillMount函数。
3、调用render函数来渲染组件,这个函数一定会有一个返回值,返回的是一个React元素或者null。这里仅仅只是返回一个React元素,并没有将组件挂载到DOM树上。一般我们在这里可以通过this.state和this.props来控制返回的React元素。

4、调用componentDidMount函数,这个函数会在组件被挂载到DOM树之后调用。一般我们会在这里通过请求来获取数据和绑定事件监听函数。

class App extends Component {
	constructor (props) {
		super();
		console.log(props);
		// 这里进行组件state初始化操作
		this.state = {
			name : 'andy'
		}
		// 绑定成员函数this
		this.handleClick = this.handleClick.bind(this);
	}
	handleClick () {
		this.setState({
			name : 'jack'
		});
	}
	componentDidMount () {
		// 发送请求获取数据
		axios.get('xxx')
		.then(res => {

		})
		.catch(err => {

		})
	}
	render () {
		const { name } = this.state;
		return (
			<div>
				<button onClick={this.handleClick}>click here</button>
				<div>{name}</div>
			</div>
		)
	}
}

更新过程

我们通过修改组件的props和state来更新组件。更新过程会触发以下声明周期函数:

1、当props改变时,会触发componentWillReceiveProps函数。

2、会调用shouldComponentUpdate(nextProps , nextState)函数,这个函数接受两个参数,一个是改变后的props,一个是改变后的state,调用该返回必须返回一个布尔值,如果是false,表示不需要重新渲染组件(不会调用render函数),如果是true,表示需要重新渲染组件(会调用render函数)。一般会在这里进行优化。

class Button extends Component {
    constructor (props) {
        super(props);
        this.state = {
            tick : 0
        };
        console.log("组件初始化");
    }
    onClickHandle () {
        this.setState({
            tick : ++this.state.tick
        });
        this.props.onHandleClick();
    }
    shouldComponentUpdate (nextProps , nextState) {
        console.log('组件是否需要更新');
        // 通过返回值来表示组件是否需要重新渲染
        return true;
    }
    componentDidUpdate () {
        console.log('组件已经更新');
    }
    componentDidMount () {
        console.log('组件已经挂载');
    }
    render () {
        console.log('渲染');
        const { color } = this.props;
        return <button onClick={() => this.onClickHandle()} style={{color : `${color}`}}>{this.state.tick}</button>
    }
}

class App extends Component {
    constructor (props) {
        super(props);
        this.state = {
            color : 'black'
        }
    }
    handleClick = () => {
        this.setState({
            color : 'red'
        })
    }
    render () {
        return (
            <div>
                <h1>hello andy</h1>
                <h1 style={{color : 'red'}}>hello peter</h1>
                <Button onHandleClick={this.handleClick} color={this.state.color} name="点我" />
            </div>
        )
    }
}

上面的shouldComponentUpdate函数中,如果返回false,那么当我们点击按钮时,不会加1,因为不会调用render函数进行重新渲染,并且也不会触发componentDidUpdate函数,如果返回true,那么点击按钮时,会加1。

3、如果shouldComponentUpdate函数返回true,那么会调用componentWillUpdate函数。

4、调用render函数,进行渲染。这个地方会重新创建react元素,并绑定最新的props和state。

class App extends Component {
    constructor (props) {
        super(props);
        this.state = {
            tick : 0
        };
        console.log("组件初始化");
    }
    onClickHandle () {
        this.setState({
            tick : ++this.state.tick
        });
    }
    shouldComponentUpdate (nextProps , nextState) {
        console.log('组件是否需要更新');
        return true;
    }
    componentDidUpdate () {
        console.log('组件已经更新');
    }
    componentDidMount () {
        console.log('组件已经挂载');
    }
    render () {
        console.log('渲染');
        // 这里获取的是最新的state
        console.log(this.state);
        return <button onClick={() => this.onClickHandle()}>{this.state.tick}</button>
    }
}

5、会调用componentDidUpdate函数,这个函数表示React组件已经更新完成了。

卸载过程

当我们不需要一个组件时,我们可以把组件从DOM树上删除,在删除前的这个时候会触发componentWillUnmount函数。在这个函数我们一般会去解绑事件,删除定时器,或者其他一些没有用的变量等,主要是为了防止内存泄漏。

class Button extends Component {
    constructor (props) {
        super(props);
        console.log("组件初始化");
    }
    shouldComponentUpdate (nextProps , nextState) {
        console.log('组件是否需要更新');
        return true;
    }
    componentDidUpdate () {
        console.log('组件已经更新');
    }
    componentDidMount () {
        console.log('组件已经挂载');
        window.addEventListener('resize' , this.handleResize);
    }
    componentWillUnmount () {
        console.log('组件将被卸载');
        window.removeEventListener('resize' , this.handleResize);
    }
    handleResize = () => {
        console.log('改变窗口大小触发该事件');
    }
    render () {
        console.log('渲染');
        return (
            <button onClick={() => this.onClickHandle()}>click here</button>
        )
    }
}

class App extends Component {
    constructor (props) {
        super(props);
        this.state = {
            show : true
        }
    }
    deleteComponent = () => {
        this.setState({
            show : false
        })
    }
    render () {
        return (
            <div>
                <button onClick={this.deleteComponent}>删除组件</button>
                {
                    this.state.show ? <Button /> : null
                }
            </div>
        )
    }
}

注意点

在16.3版本以上的React中,componentWillMount,componentWillReceiveProps,componentWillUpdate将被删除,当然还是可以用的,只是被标记了不安全,今后应该会被删除。

Vue-Router基础

Vue-Router基础

Vue-Router是Vuejs的路由管理器。

import Vue from 'vue';
import VueRouter from 'vue-router';
import About from './components/About.vue';
import Home from './components/Home.vue';
Vue.use(VueRouter);
const router = new VueRouter({
    routes : [
        {
            path : '/home',
            component : Home,
            name : 'Home'
        },
        {
            path : '/about',
            component : About,
            name : 'About'
        }
    ]
});
export default router;

有了路由,vuejs可以很容易的搭建单页应用项目,我们可以将不同的路由路径映射到不同的组件。

动态路由匹配

我们经常需要把某种模式匹配的所有路由都全部映射到同一个组件,比如,我们有一个Company组件,对于所有的companyId的公司,都要使用这个组件来渲染,那么这个时候我们可以使用动态路径参数来实现这个效果。

{
    // 动态路径参数以冒号开头
    path : '/company/:id',
    component : Company,
    name : 'Company'
}

上面的代码中,/company/234和/company/123其实都将映射到相同的组件。那我们怎么访问到动态路径参数呢?我们使用动态路由时,当匹配到一个路由,动态路径参数就会被设置到this.$route.params上,我们可以在组件内使用。

export default {
    data () {
        return {
            companyId : ''
        }
    },
    created () {
        const id = this.$route.params.id;
        this.companyId = id;
    }
}

当使用路由参数时,如果从/company/123导航到/company/324,原来的组件实例会被复用。因为这两个路由都是渲染同一个组件,比起销毁再重新创建,复用的效率要高一点。不过这也意味着,组件的生命周期钩子不会再被调用。

<template>
    <div>
        公司ID为{{companyId}}
    </div>
</template>
<script>
    export default {
        data () {
            return {
                companyId : ''
            }
        },
        created () {
            const id = this.$route.params.id;
            this.companyId = id;
        }
    }
</script>

上面代码中,当我们匹配到不同的路由时,都是渲染同一个组件,但是组件显示的公司id永远都是第一个匹配路由的id,因为组件被复用,所以不会再调用生命周期钩子,那此时我们应该怎么去获取到不同路由下的公司id呢?其实我们可以通过监听$route的变化来获取。

<template>
    <div>
        公司ID为{{companyId}}
    </div>
</template>
<script>
    export default {
        data () {
            return {
                companyId : ''
            }
        },
        created () {
            const id = this.$route.params.id;
            this.companyId = id;
        },
        watch: {
            // 这里我们可以监听$route的变化,to表示的是要跳转到的路由对象,from表示的是原路由对象
            $route (to , from) {
                this.companyId = to.params.id;
            }
        }
    }
</script>

除了上面这种方式,我们也可以使用导航守卫来实现

<template>
    <div>
        公司ID为{{companyId}}
    </div>
</template>
<script>
    export default {
        data () {
            return {
                companyId : ''
            }
        },
        created () {
            const id = this.$route.params.id;
            this.companyId = id;
        },
        // 当路由发生变化的时候,就会调用这个钩子,记得一定要调用next
        beforeRouteUpdate (to , from , next) {
            this.companyId = to.params.id;
            next();
        }
    }
</script>

我们这里要区分一下$router和$route的区别,$router表示的是VueRouter实例,$route表示的是当前的路由对象。

嵌套路由

在很多界面,通常由多层嵌套的组件组合而成。同样的,url中各段动态路径也按某种结构对应嵌套的各层组件,如果我们想要实现这种效果,那么我们可以给vue-router添加一个children属性。比如:

Vue.use(VueRouter);
const router = new VueRouter({
    routes : [
        {
            path : '/home',
            component : Home,
            name : 'Home'
        },
        {
            path : '/about',
            component : About,
            name : 'About'
        },
        {
            path : '/company/:id',
            component : Company,
            name : 'Company',
            children : [
                {
                    path : 'photo',
                    component : Photo
                }
            ]
        }
    ]
});
export default router;

这里我们需要注意的是,以"/"开头的嵌套路径会被当做是根路径,所以我们在使用的时候一定不要在children里面的path加上/。而嵌套路由中对应的组件会被渲染到父路由的组件里,所以我们只需要在父路由的组件中添加一个就可以了。

<template>
    <div>
        公司ID为{{companyId}}
        <router-view></router-view>
    </div>
</template>

编程式的导航

我们除了可以使用标签来定义导航链接之外,我们还可以使用router的实例方法,通过代码是来实现导航。

1、router.push(location , onComplete? , onAbort?)

router实例我们可以通过this.$router来访问,当我们调用push方法时,会导航到不同的url,这个方法会向history栈中添加一条记录,当我们使用浏览器的回退按钮时,就会回退到之前的url。

// App.vue文件
<template>
<div id="app">
    <div>
        <button @click="goAbout">about</button>
        <button @click="goHome">home</button>
        <button @click="goCompany">company</button>
        <button @click="goCompanyInfo">companyInfo</button>
    </div>
    <router-view></router-view>
</div>
</template>

<script>

export default {
    methods: {
        goAbout () {
            this.$router.push('/about');
        },
        goHome () {
            this.$router.push({path : '/home'});
        },
        goCompany () {
            this.$router.push({
                name : 'Company',
                params : {id : 12}
            })
        },
        goCompanyInfo () {
            this.$router.push({
                name : 'Photo',
                params : {id : 3242 , photo : 'photo'}
            })
        }
    }
}
</script>
// routes.js文件
import Vue from 'vue';
import VueRouter from 'vue-router';
import About from './components/About.vue';
import Home from './components/Home.vue';
import Company from './components/Company';
import Photo from './components/Photo';
Vue.use(VueRouter);
const router = new VueRouter({
    routes : [
        {
            path : '/home',
            component : Home,
            name : 'Home'
        },
        {
            path : '/about',
            component : About,
            name : 'About'
        },
        {
            path : '/company/:id',
            component : Company,
            name : 'Company',
            children : [
                {
                    path : 'photo',
                    component : Photo,
                    name : 'Photo'
                }
            ]
        }
    ]
});
export default router;

这里我们需要注意一点,如果location参数是一个对象,那么当对象中存在path字段,使用params字段是不会生效的,我们可以使用name字段和params字段,或者只使用一个path字段,将动态参数写入到path字段的值中。

2、router.replace(location , onComplete? , onAbort?)

这个方法和router.push方法很类似,唯一的不同就是,这个方法不会向history栈中添加一个记录,而是替换掉当前的history记录。

3、router.go(n)

这个方法主要就是在history记录中向前或向后多少步,类似window.history.go(n)方法。

命名路由

我们可以在创建Router实例的时候,在routes配置中给路由设置一个名称,这样我们在做路由跳转的时候,我们可以这样使用:

const router = new VueRouter({
  routes: [
    {
      path: '/user/:userId',
      name: 'user',
      component: User
    }
  ]
})
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>

或者

router.push({ name: 'user', params: { userId: 123 }})

命名视图

命名视图,说白了就是给视图取个名字,也就是给添加一个name属性,如果router-view没有设置name属性,那么默认为default。

<template>
<div id="app">
    <div>
        <router-link to="/all">all</router-link>
    </div>
    <router-view name="a"></router-view>
    <router-view name="b"></router-view>
</div>
</template>

<script>

export default {

}
</script>
import Vue from 'vue';
import VueRouter from 'vue-router';
import About from './components/About.vue';
import Home from './components/Home.vue';
Vue.use(VueRouter);
const router = new VueRouter({
    routes : [
        {
            path : '/all',
            components : {
                a : About,
                b : Home
            }
        }
    ]
});
export default router;

重定向

重定向也是通过routes配置来完成,比如:

Vue.use(VueRouter);
const router = new VueRouter({
    routes : [
        {
            path : '/all',
            components : {
                a : About,
                b : Home
            }
        },
        {
            path : '/home',
            redirect : '/about'
        },
        {
            path : '/about',
            component : About
        }
    ]
});
export default router;

重定向的目标也可以是一个命名的路由,比如:

Vue.use(VueRouter);
const router = new VueRouter({
    routes : [
        {
            path : '/all',
            components : {
                a : About,
                b : Home
            }
        },
        {
            path : '/home',
            redirect : {name : 'about'}
        },
        {
            path : '/about',
            name : 'about',
            component : About
        }
    ]
});
export default router;

路由组件传参

redux之中间件介绍

Redux之中间件

我们之前所讲的action只是一个普通的对象,而且action都是同步action,如果要dispatch一个异步任务,那么我们该怎么办呢?比如我们通过请求去获取数据,这个过程是异步的,我们不可能dispatch这个任务,然后再去处理返回的数据。就像这样:

// 这里都是伪代码

// 这里是发送请求去获取数据的任务,这里是异步的,所以我们在后面在分发一个处理响应的任务是根本获取不到数据的,那么又怎么去更新数据呢?
dispatch(sendRequestAction);
//这里是处理响应的任务
dispatch(handleResponseAction);

如果dispatch方法接受的是一个函数而不是一个普通对象,那么我们可以在这个函数里面进行异步操作,然后等结果返回的时候,我们再调用dispatch派发处理异步结果的action,那么就可以解决这个问题了。

redux提供了一个applyMiddleware方法来添加中间件来解决这样的问题。

applyMiddleware方法

function applyMiddleware() {
    // 处理参数,这里传入的参数都是中间件函数
    // 将所有的中间件函数保存在middlewares数组里
    /*
        middlewares = [
            middleware1,
            middleware2,
            ...
            middlewareN
        ]
    */
    for (var _len = arguments.length,     middlewares = new Array(_len), _key = 0; _key < _len; _key++) {
        middlewares[_key] = arguments[_key];
    }
    // 返回一个函数,这个函数就是createStore方法中传入的第三个参数enhancer
    // enhancer(createStore)(reducer, preloadedState)
    return function (createStore) {
        return function () {
            // 这里的arguments参数就是reducer和preloadedState这两个
            // 所以这里还是通过createStore方法创建一个store
            var store = createStore.apply(void 0, arguments);
            
            var _dispatch = function dispatch() {
                throw new Error("Dispatching while constructing your middleware is not allowed. " + "Other middleware would not be applied to this dispatch.");
            };
            
            // 中间件函数传入的两个参数
            var middlewareAPI = {
                getState: store.getState,
                dispatch: function dispatch() {
                  return _dispatch.apply(void 0, arguments);
                }
            };
            var chain = middlewares.map(function (middleware) {
                return middleware(middlewareAPI);
            });
          _dispatch = compose.apply(void 0, chain)(store.dispatch);
          return _objectSpread({}, store, {
            dispatch: _dispatch
          });
        };
    };
}

我们先来看一下中间件函数的写法:

// es6的写法
const middleware = store => next => action => {
    // 代码...
}

上面代码相当于:

// es5的写法
const middleware = function (store) {
    return function (next) {
        return function (action) {
            // 代码...
        }
    }
}

我们再来看一下applyMiddleware方法中的这段代码:

var chain = middlewares.map(function (middleware) {
    return middleware(middlewareAPI);
});

当我们遍历middlewares数组中的所有的中间件,并调用中间件,将middlewareAPI传递给中间件函数,这里调用的目的是接口(store的getState方法和dispatch方法)暴露给每一个中间件函数使用。

当我们遍历完middlewares数组中的所有中间件,将返回的中间件保存在chain遍历中,这时候的中间件函数,已经是这个样子:

const middleware = function (next) {
    return function (action) {
        // 代码...
    }
}

然后再执行compose方法。

compose方法

compose方法是将多个中间件函数从右往左组合到一起。

function compose() {
    // 将传递过来的中间件函数保存到funcs中
    for (var _len = arguments.length, funcs = new Array(_len), _key = 0; _key < _len; _key++) {
        funcs[_key] = arguments[_key];
    }
    
    // 如果没有中间件函数,那么就返回默认的函数
    if (funcs.length === 0) {
        return function (arg) {
          return arg;
        };
    }
    
    // 如果只有一个中间件函数,那么就直接返回这一个中间件函数
    if (funcs.length === 1) {
        return funcs[0];
    }
    
    // 有多个中间件函数的情况
    return funcs.reduce(function (a, b) {
        return function () {
            return a(b.apply(void 0, arguments));
        };
    });
}

这里我们重点来看一下这段代码:

return funcs.reduce(function (a, b) {
    return function () {
        return a(b.apply(void 0, arguments));
    };
});

我们假设funcs数组中存在三个中间件函数。

const funcs = [fn1 , fn2 , fn3];

当首次执行reduce方法时,这个方法中的回调函数中的参数a和b的值是:fn1 , fn2,返回一个函数是:

function () {
    return a(b.apply(void 0, arguments));
};

// 此时a为fn1,b为fn2
function () {
    return fn1(fn2.apply(void 0 , arguments));
}

// 其实就是
function () {
    return fn1(fn2(arguments));
}

当第二次执行reduce方法的回调函数时,a的值为上一次执行reduce方法的回调函数返回的值,而b则是fn3。

a = function () {
    return fn1(fn2(arguments));
};
b = fn3;

那么第二次执行的回调函数返回的值为:

function () {
    return fn1(fn2(fn3(arguments)));
}

所以调用compose方法最终返回的是:

function () {
    return fn1(fn2(fn3(arguments)));
}

我们再回过头来看applyMiddleware方法中的这段代码:

_dispatch = compose.apply(void 0, chain)(store.dispatch);

// 上面代码等价于:
_dispatch = function () {
    return fn1(fn2(fn3(argumnets)));
}(store.dispatch);

// 最终为:
_dispatch = fn1(fn2(fn3(store.dispatch)));

到这里我们首先要明确一点,就是这里的fn1 , fn2 , fn3都是已经执行过一层的中间件函数,当执行fn1(fn2(fn3(store.dispatch)))代码,我们来看一下是怎么执行的,首先会执行fn3中间件,而fn3中间件函数中的next参数就是这里的store.dispatch,返回最后一层的函数,以此类推,下一个中间件接收的next参数就是上一个中间件执行返回的结果。这样讲可能不太直观,我们来通过一个例子来进行说明:

// 三个中间件函数
const fn1 = function (store) {
    return function (next) {
        return function (action) {
            console.log('fn1');
            next(action);
            console.log('fn1');
        }
    }
}

const fn2 = function (store) {
    return function (next) {
        return function (action) {
            console.log('fn2');
            next(action);
            console.log('fn2');
        }
    }
}

const fn3 = function (store) {
    return function (next) {
        return function (action) {
            console.log('fn3');
            next(action);
            console.log('fn3');
        }
    }
}

// reducer函数
const reducer = function (state = 0 , action) {
    switch (action.type) {
        case 'add' : 
        return state + 1;
        case 'delete' :
        return state - 1;
        default :
        return state;
    }
}

const store = createStore(reducer , 0 , applyMiddleware(fn1 , fn2 , fn3));
store.subscribe(function () {
    console.log(store.getState());
});
store.dispatch({
    type : 'add'
});

具体的执行过程如下:

第一次执行时:

function (action) {
    console.log('fn3');
    dispatch(action);
    console.log('fn3'); 
};

第二次执行时:

function (action) {
    console.log('fn2');
    (function (action) {
        console.log('fn3');
        dispatch(action);
        console.log('fn3'); 
    })(action);
    console.log('fn2');
}

第三次执行时:

function (action) {
    console.log('fn1');
    (function (action) {
        console.log('fn2');
        (function (action) {
            console.log('fn3');
            dispatch(action);
            console.log('fn3'); 
        })(action);
        console.log('fn2');
    })(action);
    console.log('fn1');
}

所以最终的的dispatch的值为:

dispatch = function (action) {
    console.log('fn1');
    (function (action) {
        console.log('fn2');
        (function (action) {
            console.log('fn3');
            dispatch(action);
            console.log('fn3'); 
        })(action);
        console.log('fn2');
    })(action);
    console.log('fn1');
}

然后调用_objectSpread函数将这个dispatch合并到store对象上。所以当我们调用store.dispatch方法,其实就是执行这个dispatch。

上面代码中,当调用store.dispatch()方法派发一个任务时,才开始真正的执行中间件函数,首先执行中间件fn1,当执行到next时,就会执行fn2中间件,以此类推直到最后一个中间件,然后才调用原生的store.dispatch方法。

image

image

总结:

中间件可以让我们在派发一个action到reducer函数处理action并更新state这个中间的过程中,可以执行更多的操作。

react的更新流程

react的更新流程

当我们调用react组件的setState方法时,就会执行react的更新流程,它只会更新组件有改动的部分。

setState方法是Component类的一个原型方法,而我们在创建react组件的时候,都会继承Component类,所以每个react组件都具有这个方法

class App extends Component {
    constructor (props) {
        super(props);
    }
}

而setState方法是在react这个库中,不是在react-dom的库中,我们知道react这个库主要做的事情就是创建React元素。当创建完React元素,再调用ReactDOM的render方法来进行初始化渲染。

既然两个库都是分开的,那么当调用setState方法时,react-dom库中是怎么来进行更新的?我们可以看一下这段代码:

// react库中代码
Component.prototype.setState = function (partialState, callback) {
    // 省略...
    this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

其实当我们调用setState方法时,内部调用的是this.updater.enqueueSetState。

function constructClassInstance () {
    //代码省略...
    // 调用组件的构造函数,返回一个组件实例
    var instance = new ctor(props, context);
    var state = workInProgress.memoizedState = instance.state !== null && instance.state !== undefined ? instance.state : null;
    adoptClassInstance(workInProgress, instance);
    //代码省略...
}

function adoptClassInstance(workInProgress, instance) {
    // 将classComponentUpdater对象挂载到实例的updater属性上。
    instance.updater = classComponentUpdater;
    workInProgress.stateNode = instance;
    set(instance, workInProgress);
    {
        instance._reactInternalInstance = fakeInternalInstance;
    }
}

当我们调用ReactDOM.render方法进行初始化渲染的时候,在这个初始化的过程中,其中就会调用到constructClassInstance,该方法组件是初始化组件实例,并将组件的state属性保存到FiberNode的memoizedState上,然后调用adoptClassInstance方法。

adoptClassInstance方法,主要就是将classComponentUpdater对象挂载到实例的updater属性上。这样当我们调用setState方法时,内部执行this.updater.enqueueSetState方法,从而就会调用classComponentUpdater中的enqueueSetState方法。所以当我们调用this.setState方法时,其实就是执行classComponentUpdater.enqueueSetState方法。

enqueueSetState
// 这个方法接受三个参数
// inst :表示的是组件实例
// payload :表示的是调用setState传入的第一个参数(即:要更新的数据)
// callback : 表示的是调用setState传入的第二个参数
enqueueSetState: function (inst, payload, callback) {
    // 获取对应组件实例的FiberNode
    var fiber = get(inst);
    // 当前时间
    var currentTime = requestCurrentTime();
    // 过期时间
    var expirationTime = computeExpirationForFiber(currentTime, fiber);
    // 创建一个update对象
    var update = createUpdate(expirationTime);
    // 将需要更新的数据挂载到update对象的payload属性上
    update.payload = payload;
    // 如果有传入callback参数,那么将callback参数挂载到update的callback属性上
    if (callback !== undefined && callback !== null) {
        {
            warnOnInvalidCallback$1(callback, 'setState');
        }
        update.callback = callback;
    }
    
    flushPassiveEffects();
    // 将update放到update队列中
    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
}
updateQueue

updateQueue是一个普通的对象,包含以下主要属性:

属性名 类型 描述
baseState Object 表示更新前的基础状态
firstUpdate Update 表示第一个update对象引用,总体是一个单链表结构
lastUpdate Update 表示最后一个update对象引用,总体是一个单链表结构
firstEffect Update 表示第一个包含副作用(callback)的update对象的引用
lastEffect Update 表示最后一个包含副作用(callback)的update对象引用
appendUpdateToQueue

当我们调用enqueueUpdate方法时,该方法内部会调用appendUpdateToQueue方法

function appendUpdateToQueue(queue, update) {
  // 如果queue对象的lastUpdate属性为空,那么表示updateQueue队列是空的,那么我们将update对象挂载到updateQueue的firstUpdate和lastUpdate属性上,表示第一个更新的对象和最后一个更新的对象都是同一个
  // 如果之前的updateQueue队列不为空,那么就将update对象挂载到updateQueue的最后一个update对象的下一个,并且更新updateQueue的lastUpdate为当前的update对象
  if (queue.lastUpdate === null) {
    // Queue is empty
    queue.firstUpdate = queue.lastUpdate = update;
  } else {
    queue.lastUpdate.next = update;
    queue.lastUpdate = update;
  }
}
测试用例:
import React, { Component } from 'react';

class App extends Component {
    constructor (props) {
        super(props);
        this.state = {
            count : 0
        }
    }
    add = (evt) => {
        this.setState({
            count : this.state.count + 1
        })
    }
    render () {
        return (
            <div>
                <p>{this.state.count}<span>3245</span></p>
                <button onClick={this.add}>add</button>
            </div>
        )
    }
}

export default App;

当我们首次渲染完之后,再点击按钮时,会触发add事件回调函数,然后会执行setState方法,setState方法内部主要就是创建一个update对象,然后将这个update对象挂载到updateQueue中,当事件回调函数执行完之后,才会执行具体的组件更新操作。

if (!isBatchingUpdates && !isRendering) {
  performSyncWork();
}

当执行完事件回调函数之后,这个时候事件回调已经执行完了,所以isBatchingUpdates是false,而当页面首次渲染完之后,isRendering也是fasle(页面也没有进行其他渲染),所以这个时候就会执行performSyncWork方法,处理组件更新。

performSyncWork方法—>performWork方法—>performWorkOnRoot方法—>renderRoot方法—>workLoop方法—>performUnitOfWork方法—>beginWork方法

当循环调用performUnitOfWork方法完之后,就会调用completeUnitOfWork方法

我们发现这里更新组件的方式和组件在进行第一次初始化的时候类似。这里主要的更新工作都是在==beginWork方法==中进行。

beginWork

在beginWork方法中,主要是做以下几件事情(这里以上面的例子为例来说明):

  • 判断fiberNode的tag值,根据不同的tag值,做相应的处理。不同的tag值,代表不同的React元素类型。从上面的例子中,我们看出App是一个classComponent,所以会调用updateClassComponent方法。
  • updateClassComponent方法内部会判断当前的FiberNode是否存在stateNode属性,如果不存在,表示该组件是第一次渲染,如果存在,那么表示该组件不是第一次渲染,而是调用setState方法来执行组件更新操作,这个时候会调用updateClassInstance方法。
  • 从命名中我们可以看出,updateClassInstance方法就是更新组件实例。内部主要是通过fiberNode.updateQueue获取当前fiberNode的更新队列,然后调用processUpdateQueue方法,来更新队列中的数据。
  • processUpdateQueue方法内部,先是克隆一个updateQueue的副本,然后获取updateQueue中的firstUpdate对象(update对象里面的payload属性就保存了调用setState方法需要更新的数据),然后循环遍历firstUpdate对象(注意:update对象是一个单链表结构,里面有一个next属性,表示的是下一个update对象,当更新完第一个update对象,那么就会通过update.next获取下一个update对象,直到update.next的值为null)
  • 具体的update更新是执行getStateFromUpdate方法,在getStateFromUpdate方法内部,通过判断update对象的tag值来执行相应的更新(update对象有四种更新类型,当前有0~3,分别是UpdateState、ReplaceState、ForceUpdate、CaptureUpdate),这里的更新主要是调用_assign({}, prevState, partialState)方法,将对象进行合并,返回一个合并后的新对象。当更新完update对象后,又会判断调用setState方法时,有没有传入第二个参数(即回调函数),如果有,那么会将这个update添加到updateQueue的firstEffect和lastEffect上。
  • 通过update.next获取下一个update对象,执行上面的操作,直到update.next的值为null为止。
  • 当所有的update更新完之后,就会将更新后的数据赋值给updateQueue.baseState,清空updateQueue中的firstUpdate,lastUpdate。并将更新后的数据赋值给fiberNode.memoizedState,将currentlyProcessingQueue赋值为null,表示当前正在处理的队列已经处理完了。
  • 处理完updateQueue之后,那么表示更新数据的操作就已经执行完了,这个时候就会调用getDerivedStateFromProps生命周期钩子。
  • 然后调用checkShouldComponentUpdate方法来检查是否需要更新组件,如果组件存在shouldComponentUpdate方法,内部会调用生命周期函数shouldComponentUpdate方法,这里如果调用shouldComponentUpdate方法,就必须返回一个结果(一般是true或false),如果没有返回结果,那么会抛出异常,默认是返回true,需要更新组件。
  • 如果需要更新组件,那么shouldUpdate值为true,那么就会调用componentWillUpdate生命周期钩子。
  • 最后会更新组件实例的props,state,context。
  • 然后调用finishClassComponent方法,这个方法内部会判断shouldUpdate的值,如果是false,那么表示组件不需要更新,直接调用bailoutOnAlreadyFinishedWork,并返回,如果为true,那么就会调用组件市里的render方法,返回一个React元素。

beginWork方法,主要做的工作就是计算数据更新。当所有的更新都做完之后,那么才会进入到diff阶段,找出元素中更新的部分进行更新。

reconcileChildren方法

diff阶段,就是通过调用这个方法来协调子节点,找出需要更新的部分,进行更新。在diff过程中,会根据子节点的tag属性来判断当前节点的类型,然后执行相应的操作。以我们当前的例子为例,首先会计算App,然后计算App的第一个子节点(div),然后是div的第一个子节点(p)

  • App:tag属性值是ClassComponent,所以执行updateClassComponent方法,这里表示的是React元素
  • div:tag属性值是HostComponent,所以执行updateHostComponent方法,这里就表示的是html元素
  • p:tag属性值是HostComponent,所以执行updateHostComponent方法,这里就表示的是html元素
    • p的children属性的值是一个数字1,那么nextChildren设置为null,再调用reconcileChildren方法,这个时候表示p下面是没有子节点的,所以不需要协调,直接返回null。

updateHostComponent方法

function updateHostComponent(current$$1, workInProgress, renderExpirationTime) {
    pushHostContext(workInProgress);

    if (current$$1 === null) {
    tryToClaimNextHydratableInstance(workInProgress);
    }
    
    // 获取相关属性值
    var type = workInProgress.type;
    // 获取下一个props对象
    var nextProps = workInProgress.pendingProps;
    // 获取上一个props对象
    var prevProps = current$$1 !== null ? current$$1.memoizedProps : null;
    // 获取子节点
    var nextChildren = nextProps.children;
    // 判断这个子节点是不是文本节点,如果是,为true,否则,为false
    var isDirectTextChild = shouldSetTextContent(type, nextProps);
    
    // 如果这个子节点是一个文本节点,那么nextChildren设置为null,表示该节点的下面已经没有子节点了
    if (isDirectTextChild) {
        nextChildren = null;
    // 如果在更新之前这个节点的子节点是一个文本节点,那么我们需要重置文本内容
    } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
        // If we're switching from a direct text child to a normal child, or to
        // empty, we need to schedule the text content to be reset.
        workInProgress.effectTag |= ContentReset;
    }

    markRef(current$$1, workInProgress); // Check the host config to see if the children are offscreen/hidden.

    if (renderExpirationTime !== Never && workInProgress.mode & ConcurrentMode && shouldDeprioritizeSubtree(type, nextProps)) {
        // Schedule this fiber to re-render at offscreen priority. Then bailout.
        workInProgress.expirationTime = workInProgress.childExpirationTime = Never;
        return null;
    }
    // 协调子节点
    reconcileChildren(current$$1, workInProgress, nextChildren, renderExpirationTime);
    return workInProgress.child;
}

function reconcileChildren(current$$1, workInProgress, nextChildren, renderExpirationTime) {
    // 如果是没有渲染的新的组件,那么我们就直接将它添加到workInProgress子节点中,就不用去diff了
    // 这里我们需要注意的是:nextChildren变量的值是通过调用instance.render()返回的
    if (current$$1 === null) {
        workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderExpirationTime);
    } else {
        workInProgress.child = reconcileChildFibers(workInProgress, current$$1.child, nextChildren, renderExpirationTime);
    }
}
reconcileChildFibers
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, expirationTime) {

    var isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null;

    if (isUnkeyedTopLevelFragment) {
      newChild = newChild.props.children;
    } // Handle object types
    
    // 如果新的子节点是一个对象
    var isObject = typeof newChild === 'object' && newChild !== null;

    // 如果子节点是React元素或者ReactDOM.createPortal创建的元素
    if (isObject) {
        switch (newChild.$$typeof) {
            case REACT_ELEMENT_TYPE:
            return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, expirationTime));
        
            case REACT_PORTAL_TYPE:
            return placeSingleChild(reconcileSinglePortal(returnFiber, currentFirstChild, newChild, expirationTime));
        }
    }
    
    // 如果子节点是一个字符串或者数字,那么表示该节点是一个文本节点
    if (typeof newChild === 'string' || typeof newChild === 'number') {
        return placeSingleChild(reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, expirationTime));
    }
    
    // 如果子节点是一个数组,那么表示该节点是一个集合(比如说列表项)
    if (isArray(newChild)) {
        return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, expirationTime);
    }

    // 如果子节点是一个迭代器
    if (getIteratorFn(newChild)) {
        return reconcileChildrenIterator(returnFiber, currentFirstChild, newChild, expirationTime);
    }
    
    // 如果子节点是一个普通的对象,那么就抛出异常
    if (isObject) {
        throwOnInvalidObjectType(returnFiber, newChild);
    }   
    
    // 如果子节点是一个函数,那么也抛出异常,函数不是一个有效的react元素
    {
        if (typeof newChild === 'function') {
            warnOnFunctionType();
        }
    }
    
    if (typeof newChild === 'undefined' && !isUnkeyedTopLevelFragment) {
        switch (returnFiber.tag) {
            case ClassComponent:
            {
                {
                    var instance = returnFiber.stateNode;
                    
                    if (instance.render._isMockFunction) {
                        // We allow auto-mocks to proceed as if they're returning null.
                        break;
                    }
                }
            }
            case FunctionComponent:
            {
                var Component = returnFiber.type;
                invariant(false, '%s(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.', Component.displayName || Component.name || 'Component');
            }
        }
    } // Remaining cases are all treated as empty.
    // 删除除了第一个子节点之外的剩余节点
    return deleteRemainingChildren(returnFiber, currentFirstChild);
}
function reconcileSingleElement(returnFiber, currentFirstChild, element, expirationTime) {
    var key = element.key;
    var child = currentFirstChild;
    
    while (child !== null) {
        if (child.key === key) {
            // 如果不是Fragment,那么就判断当前的第一个子节点和新的子节点的类型是否一样
            // 如果一样,那么就调用deleteRemainingChildren方法,删除剩余的子节点(这里的剩余的子节点,指的是除了第一个子节点之外的所有的其他兄弟节点),如果没有兄弟节点,就不用管它
            if (child.tag === Fragment ? element.type === REACT_FRAGMENT_TYPE : child.elementType === element.type) {
              deleteRemainingChildren(returnFiber, child.sibling);
              // 调用useFiber方法,克隆一个child副本,并将新的react元素的props(也就是children属性的值)挂载到新建的FiberNode的peddingProps上。
              var existing = useFiber(child, element.type === REACT_FRAGMENT_TYPE ? element.props.children : element.props, expirationTime);
              // 更新,新创建的FiberNode的属性
              existing.ref = coerceRef(returnFiber, child, element);
              existing.return = returnFiber;
              {
                existing._debugSource = element._source;
                existing._debugOwner = element._owner;
              }
              return existing;
            } else {
              deleteRemainingChildren(returnFiber, child);
              break;
            }
        } else {
            deleteChild(returnFiber, child);
        }
        child = child.sibling;
    }

    if (element.type === REACT_FRAGMENT_TYPE) {
        var created = createFiberFromFragment(element.props.children, returnFiber.mode, expirationTime, element.key);
        created.return = returnFiber;
        return created;
    } else {
        var _created4 = createFiberFromElement(element, returnFiber.mode, expirationTime);
        
        _created4.ref = coerceRef(returnFiber, currentFirstChild, element);
        _created4.return = returnFiber;
        return _created4;
    }
}
function placeSingleChild(newFiber) {
    // 如果newFiberNode.alternate为null,那么表示这个节点是新加入的,我们只需要将它插入进来就可以了
    if (shouldTrackSideEffects && newFiber.alternate === null) {
        newFiber.effectTag = Placement;
    }
    return newFiber;
}

reconcileChildrenArray方法

该方法表示的是协调当前元素节点下面的子元素是数组的情况,比如说当前元素下面包含多个子元素。我们来看一下源码部分(重点部分):

for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
    } else {
        nextOldFiber = oldFiber.sibling;
    }
    // ...代码省略
    // 调用updateSlot方法,更新Fiber,其实内部就是重新创建了一个新的FiberNode,返回的是一个新的FiberNode。
    var newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], expirationTime);
    
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    
    // 一开始遍历的时候,previousNewFiber是null,所以previousNewFiber的值就是第一个返回的新的FiberNode,之后每次返回的新的FiberNode都是previousNewFiber的兄弟节点
    if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
    } else {
        previousNewFiber.sibling = newFiber;
    }
    // 更新previousNewFiber为当前的新的FiberNode
    previousNewFiber = newFiber;
    // 更新oldFiber
    oldFiber = nextOldFiber;
}

// 如果oldFiber为null,那么表示更多的子元素了,我们可以选择一条更便捷的路,因为其余的新的子元素都将是插入到元素中
if (oldFiber === null) {
    for (; newIdx < newChildren.length; newIdx++) {
        // 调用createChild方法来创建一个新的子FiberNode
        var _newFiber = createChild(returnFiber, newChildren[newIdx], expirationTime);
    
        if (!_newFiber) {
          continue;
        }
        // 如果是插入元素的话,那么这个lastPlacedIndex还是在原来的位置上面
        lastPlacedIndex = placeChild(_newFiber, lastPlacedIndex, newIdx);
    
        if (previousNewFiber === null) {
          // TODO: Move out of the loop. This only happens for the first run.
          resultingFirstChild = _newFiber;
        } else {
          previousNewFiber.sibling = _newFiber;
        }
    
        previousNewFiber = _newFiber;
    }
    // 当协调完所有子元素后,就返回协调完后的新的FiberNode,这里其实我们可以通过FiberNode.siblings去查看所有的兄弟节点
    return resultingFirstChild;
} 


// returnFiber:父FiberNode
// oldFiber:旧的子元素
// newChild:新的子元素
// expirationTime:过期时间
function updateSlot(returnFiber, oldFiber, newChild, expirationTime) {
    // 获取旧的子元素的key值,如果没有,默认为null,用来与新的子元素的key值进行比较,如果相同,那么表示是同一个子元素
    var key = oldFiber !== null ? oldFiber.key : null;
    // 文本节点是没有key的,直接更新节点就可以了
    if (typeof newChild === 'string' || typeof newChild === 'number') {
        if (key !== null) {
            return null;
        }
        return updateTextNode(returnFiber, oldFiber, '' + newChild, expirationTime);
    }
    // 如果新的子元素是一个对象,那么就会根据子元素的$$typeof判断这个新的子元素是react.element还是react.portal,
    // 如果是react.element,那么就会判断就的子元素和新的子元素的key是否相同,如果相同,那么表示是同一个元素,并且新的子元素的type不是fragment,那么就会调用updateElement方法来更新这个react元素
    //updateElement方法内部主要是调用useFiber方法,useFiber方法主要是调用workProgress方法将新的peddingProps属性传入,来重新创建一个fiberNode,并返回
    if (typeof newChild === 'object' && newChild !== null) {
        switch (newChild.$$typeof) {
            case REACT_ELEMENT_TYPE:
            {
                if (newChild.key === key) {
                  if (newChild.type === REACT_FRAGMENT_TYPE) {
                        return updateFragment(returnFiber, oldFiber, newChild.props.children, expirationTime, key);
                  }
                
                  return updateElement(returnFiber, oldFiber, newChild, expirationTime);
                } else {
                    return null;
                }
            }
        
            case REACT_PORTAL_TYPE:
            {
                if (newChild.key === key) {
                      return updatePortal(returnFiber, oldFiber, newChild, expirationTime);
                } else {
                    return null;
                }
            }
        }
        if (isArray(newChild) || getIteratorFn(newChild)) {
            if (key !== null) {
              return null;
            }
        
            return updateFragment(returnFiber, oldFiber, newChild, expirationTime, null);
        }
        
        throwOnInvalidObjectType(returnFiber, newChild);
    }
    
    {
        if (typeof newChild === 'function') {
            warnOnFunctionType();
        }
    }
    return null;
}


function placeChild(newFiber, lastPlacedIndex, newIndex) {
    // 给FiberNode.index赋值,通过这个值可以知道当前的FiberNode在所有的子FiberNode中位于哪个位置
    newFiber.index = newIndex;
    
    if (!shouldTrackSideEffects) {
      // Noop.
      return lastPlacedIndex;
    }
    // 获取FiberNode的alternate,如果没有,表示这个FiberNode是新创建的
    var current$$1 = newFiber.alternate;
    // 判断是否存在FiberNode.alternate,如果存在,表示这个FiberNode不是新创建的,而是已经存在的,那么就会获取到这个FiberNode.index值(这个值就是当前FiberNode在所有子元素中所处的位置),并将这个值赋值给oldIndex
    // 比较oldIndex和lastPlacedIndex的值
    // 如果不存在FiberNode.alternate,那么表示这个FiberNode是新创建的FiberNode,那么表示这个新FiberNode是要插入进来的FiberNode,这个时候会设置这个新的FiberNode.effectTag的值为Placement(表示插入)
    if (current$$1 !== null) {
      var oldIndex = current$$1.index;
    
      if (oldIndex < lastPlacedIndex) {
        // This is a move.
        newFiber.effectTag = Placement;
        return lastPlacedIndex;
      } else {
        // This item can stay in place.
        return oldIndex;
      }
    } else {
      // This is an insertion.
      newFiber.effectTag = Placement;
      return lastPlacedIndex;
    }
}
小结:

reconcileChildrenArray方法主要做了以下几件事:

  • 首先检查key,我们在写列表项的时候,都会给列表元素添加一个key属性,来区别不同的列表元素。
  • 然后从第一个子元素开始遍历所有的子元素,并调用updateSlot方法,进行更新操作,其实就是创建一个新的FiberNode。
  • 调用placeChild方法,来找出替换位置,方法内部主要是通过FiberNode.index与lastPlacedIndex相比较来表示,当前的这个节点是有移动,还是在原位置上,还是新插入的节点。
  • 如果遍历到最后一个子元素,那么它的siblings是为空的,这个时候会判断是否有新的节点插入进来,如果有的话,那么就创建一个新的节点,然后将新的节点挂载到最后一个子元素的siblings属性上面,最后返回更新后的该元素的第一个子元素(子元素中有siblings属性,可以通过这个属性找到所有的兄弟元素)
  • 将返回的这个新的子元素挂载到其父元素的child属性上(即:workInProgress.child)

http缓存

http缓存

作为前端开发者,当我们打开谷歌浏览器的控制台,会看到很多文件加载,并且有些文件的加载会出现"from memory cache"或"from disk cache"等,而且加载的时间为0毫秒。为什么加载资源没有花费时间呢?这就是http缓存在起作用,当我们在下载某些静态资源的时候,我们可以通过设置一些响应头信息来让浏览器缓存已经加载过的资源。

这里的http缓存测试,使用的nodejs来测试。

Cache-Control

通过设置"Cache-Control"响应头来通知浏览器是否缓存已加载的资源。

  • 1、如果设置"Cache-Control"的值为"no-cache",则表示浏览器不缓存资源,每次获取资源都要向服务器获取。
// nodejs代码
const http = require('http');
const fs = require('fs');
const url = require('url');
function renderHtml (path , res) {
    const reader = fs.createReadStream(path);
    reader.pipe(res);
};

function getJs (path , res) {
    const reader = fs.createReadStream(path);
    res.writeHead(200 , {
        'Cache-Control' : 'no-cache'
    });
    reader.pipe(res);
};
const server = http.createServer(function (req , res) {
    var reqUrl = req.headers.host + req.url;
    var parseUrl = url.parse(reqUrl);
    switch (parseUrl.pathname) {
        case '/':
            renderHtml('./index.html' , res);
        break;
        case '/static/jquery.js':
            getJs('./static/jquery.js' , res);
        break;
        default:
            res.end('');
        break;
    }
});

server.listen(8000 , '127.0.0.1' , function () {
    console.log('listening port 8000');
});
//index.html代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>nodejs</title>
    <script src="/static/jquery.js"></script>
</head>
<body>
    <h1>welcome to nodejs</h1>
</body>
</html>

当我们创建好一个web本地服务器,然后输入 http://localhost:8000时 ,我们打开控制台,会发现每次刷新页面,jquery.js这个文件总是会去从服务器上重新获取,浏览器不并不会缓存它。

  • 2、当我们设置"Cache-Control"的值为"max-age=xxx"时,则浏览器会缓存已加载的资源,并且会缓存xxx秒,xxx秒之后,缓存失效,浏览器又会重新从服务器上获取,在此之前是不会向服务器发送请求去获取该资源。
// 相同的代码就不写了
function getJs (path , res) {
    const reader = fs.createReadStream(path);
    res.writeHead(200 , {
        "Cache-Control" : 'max-age='+10
    });
    reader.pipe(res);
};

这里我们设置了max-age=10,表示浏览器加载完资源会缓存资源10秒中,10秒之内再次请求该资源,浏览器会从缓存中获取,而不会从服务器上获取,所以在控制台中会出现"from memory cache"字样

Expires

我们设置Expires响应头信息来让浏览器缓存资源,Expires从字面意思理解就是过期时间,我们可以设置一个时间,只要加载的资源在过期时间之内,那么浏览器就会从缓存中获取,而不会向服务器请求资源。但是一般我们都会使用"Cache-Control"来设置,Expires不太准,反正我试过确实不准。

// 相同的代码就不写了,直接拷最上面的代码就可以了
function getJs (path , res) {
    const reader = fs.createReadStream(path);
    res.writeHead(200 , {
        "Expires" : 'Tue, 17 Jul 2018 14:30:00 GMT'
    });
    reader.pipe(res);
};

当我们设置了Expires,那么在这个时间之前,浏览器加载的资源会被缓存,重新加载时会判断时间是否已经过期,如果没有过期,那么就从缓存中获取,如果过期,就从服务器请求资源。当我们给Expires设置一个过期时间,资源是不会被缓存的。

Cache-Control和Expires的优先级

当同时设置了"Cache-Control"和"Expires",那么谁的优先级更高呢?"Cache-Control"的优先级会更高,而"Expires"会被忽略。

function getJs (path , res) {
    const reader = fs.createReadStream(path);
    res.writeHead(200 , {
        "Cache-Control" : 'max-age='+10,
        "Expires" : 'Tue, 17 Jul 2017 14:30:00 GMT'
    });
    reader.pipe(res);
};

上面代码中,我们同时设置了"Cache-Control"和"Expires"值,一个是缓存10秒,一个是过期时间,当我们在浏览器控制台上发现,资源会被缓存10秒,10秒之后过期,重新向浏览器发起请求获取资源。

除此之外,我们也会经常看到响应头中会存在"Pragma"字段,它用来向后兼容只支持 HTTP/1.0 协议的缓存服务器,那时候 HTTP/1.1 协议中的 Cache-Control 还没有出来。 一般会看到"Pragma"被设置为"no-cache",该字段的意思和"Cache-Control"设置为"no-cache"一样。但是它们还是有优先级的,"Pragma"的优先级高于"Cache-Control"。

function getJs (path , res) {
    const reader = fs.createReadStream(path);
    res.writeHead(200 , {
        "Cache-Control" : 'max-age='+10,
        "Pragma" : 'no-cache',
        "Expires" : 'Tue, 17 Jul 2017 14:30:00 GMT'
    });
    reader.pipe(res);
};

我们在控制台中会发现,每次加载资源,浏览器都会向服务器发请求获取。

Last-Modified 和 If-Modified-Since

我们可以使用Last-Modified 和 If-Modified-Since来进行缓存协商,当Last-Modified 和 If-Modified-Since的值是一样的,表示资源的最后一次修改时间是一样的,那么资源是没有被修改,我们可以直接使用之前的资源,这个时候就没有必要去重新让服务器把所有资源又返回给客户端,只要把响应头信息返回就可以了

const http = require('http');
const fs = require('fs');
const url = require('url');

function renderHtml (path , res) {
    const reader = fs.createReadStream(path);
    reader.pipe(res);
};

function getJs (path , req , res) {
    fs.stat(path , function (err , stat) {
        if (err) {
            res.end('该文件不存在');
        } else {
            const mtime = stat.mtime.toDateString().slice(0 , 3) + ', ' + stat.mtime.getDate() + ' ' + stat.mtime.toDateString().slice(4 , 7) + ' ' + stat.mtime.getFullYear() + ' ' + stat.mtime.toLocaleTimeString() + ' GMT'
            // 如果两者相同,表示服务器上的资源没有被修改,那么就直接返回响应头信息,内容为空即可。
            if (req.headers['if-modified-since'] == mtime) {
                res.writeHead(304 , {
                    'Last-Modified' : mtime
                });
                res.end('');
            } else {
                // 如果两者不相同,那么表示服务器上的资源被修改过,那么就重新获取服务器上的资源
                const reader = fs.createReadStream(path);
                res.writeHead(200 , {
                    'Last-Modified' : mtime
                })
                reader.pipe(res);
            }
        }
    })
};
const server = http.createServer(function (req , res) {
    var reqUrl = req.headers.host + req.url;
    var parseUrl = url.parse(reqUrl);
    switch (parseUrl.pathname) {
        case '/':
            renderHtml('./index.html' , res);
        break;
        case '/static/jquery.js':
            getJs('./static/jquery.js' , req , res);
        break;
        default:
            res.end('');
        break;
    }
});

server.listen(8000 , '127.0.0.1' , function () {
    console.log('listening port 8000');
});

通过上面的代码,我们输入 http://localhost:8000 后,页面会加载jquery.js文件,如果我们修改该文件,那么服务器会返回200,而且size比较大,当我们再刷新浏览器时,发现返回的状态码是304,表示资源没有被修改,而且size比较小,只返回响应头信息,返回内容为空,浏览器会使用之前的资源。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.