React 随记
基础知识
1. setState是同步还是异步
setState 在合成事件和生命周期中是异步的,这里说的异步其实是批量更新,达到了优化性能的目的
setState 在 setTimeout 和 原生事件中是同步的,就是调用后值立刻被修改
2. 多次调用 setState 更新会被合并,如何避免
调用 setState 时,传入回调函数,而不是对象。回调的参数是最新的 state
3. 类组件的生命周期
{% raw %}
import React from 'react';
import PropTypes from 'prop-types';
class LifeCycle extends React.Component {
static defaultProps = {
msg: 'omg'
};
static propTypes = {
msg: PropTypes.string.isRequired
};
constructor(props) {
super(props);
this.state = { count: 0 };
}
// React 17 已废弃
componentWillMount() {}
componentDidMount() {}
// React 17 已废弃
// 初次渲染不会执行,只有在已挂载的组件接收新的props时才会执行
ComponentWillReceiveProps(nextProps) {}
shouldComponentUpdate(nextProps, nextState) {
return true;
}
// React 17 已废弃
componentWillUpdate() {}
// React 16.8 新增,替代ComponentWillReceiveProps
// 在shouldComponentUpdate之前执行
static getDerivedStateFromProps(nextProps, prevState) {
const { count } = state;
return count > 5 ? { count: 0 } : null;
}
// React 16.8 新增,替代componentWillUpdate
// 在render之前执行
// 此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等。
getSnapshotBeforeUpdate(prevProps, prevState) {
// 我们是否在 list 中添加新的 items ?
// 捕获滚动位置以便我们稍后调整滚动位置。
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
// 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
//(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
ComponentWillUnmount() {}
render() {
return (
<div></div>
)
}
}{% endraw %}
props.children
基本功能就不再说了。通过 this.props.children,也可以实现类似 Vue 具名插槽的功能:
实际上不会这么写,文本、事件回调等直接传 props 就可以了
{% raw %}
import React, { Component } from 'react';
class Layout extends Commponent {
render() {
const { children } = this.props;
return (
<div>
{children.content}
{children.txt}
<button onClick={children.btnClick}>按钮</button>
</div>
)
}
}
class Page extends Component {
render () {
<Layout>
{{
content: <div><h3>HomePage</h3></div>,
txt: 'hhhh',
btnClick: () => console.log('btn clicked')
}}
</Layout>
}
}{% endraw %}
redux
View -> dispatch -> action -> Reducer -> Store -> View
-| src
--| store
---| index.js
--| pages
---| Page.jsstore/index.js:
import { createStore } from 'redux';
const initState = 100;
function counterReducer(state = initState, action) {
switch(action.type) {
case 'ADD':
return state + 1;
case 'MINUS':
return state - 1;
default:
return state
}
}
const store = createStore(counterReducer)pages/Page.js:
import React from 'react';
import store from '../store'
export default class Page extends React.Component {
ComponentDidMount() {
store.subscribe(() => {
// callback: 让页面重新渲染
this.forceUpdate();
});
}
render() {
return (
<div>
<h3>ReduxPage</h3>
<p>{store.getState()}</p>
<button onClick={() => store.dispatch({ type: 'ADD' })}>加1</button>
</div>
)
}
}react-redux
react-redux 提供了两个API
- Provider
- connect
App.js:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import store from './store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)store/index.js:
略
pages/Page.js:
import { connect } from 'react-redux';
class Page {} // 略
// connect 第二个参数如果不传,会自动传递 dispatch 给 props
export default connect(
// mapStateToProps
state => ({ num: state }),
// mapDispatchToProps
{
add: () => ({ type: 'ADD' }),
minus: () => ({ type: "MINUS" })
}
)(Page);react-router-dom
react-router 包含3个库,react-router、react-router-dom和react-router-native。
常用组件:
- 是跳转的链接
是匹配的页面 是最外层 配合 Route 使用,使 Route 只能有一个被匹配
import React from 'react';
import { BrowserRouter as Router, Route, Link, Switch } from 'react-router';
class RouterPage extends React.Component {
render() {
return (
<div>
<h3>RouterPage</h3>
<Router>
<Link to="/">首页</Link>
<Link to="/user">用户</Link>
<Switch>
<Route page="/" exact component={HomePage} />
<Route page="/user" exact component={UserPage} />
<Route component={Emptypage} />
</Switch>
</Router>
</div>
);
}
}
class HomePage {}
class UserPage {}
class EmptyPage {} // 404不要这么写:component={() =>
},否则props变化时,HomePage一直重新挂载。
children: function | React.Component 优先级更高,而且它不管路由是否匹配,都会显示。
component: React.Component 优先级第二,它要匹配路由。一般都用这个。
render: functon 优先级最低。内联组件就用这个。
PureComponent
import React from 'react';
class Page extends React.Component {
state = {
count: 0
};
setCount = () => {
this.setState({ count: 100 });
}
// 使用 shouldComponentUpdate 进行性能优化
shouldComponentUpdate(nextProps, nextState) {
return nextState.count !== this.state.count;
}
render() {
console.log('render');
return (
<div>
<h3>{this.state.count}</h3>
<button onClick={this.setCount}>按钮</button>
</div>
)
}
}上面使用了shouldComponentUpdate进行性能优化,也可以直接使用PureComponent进行性能优化。
注意:PureComponent 使用了浅比较,如果上面的state有超过一层,就没有办法做到性能优化。
Hook
副作用(Side Effect):在React里,数据获取、事件订阅、手动修改DOM都属于副作用。
略
自定义Hook
是一个函数,命名要以 use 开头。
自定义时钟:
function useClock() {
const [date, setDate] = useState(new Date());
useEffect(() => {
const timer = setInterval(() => {
setDate(new Date());
}, 1000);
return () => clearInterval(timer);
}, []);
return date;
}Hook 规则:
- 只能在函数最外层调用Hook,不能在循环、条件判断或者子函数中调用
- 只能在React组件或自定义hook中使用
React 组件化
高阶组件
High Order Component
高阶组件是一个函数。它接收一个组件,返回一个新的组件。
高级组件用来给目标组件添加或修改属性。
const HocChild = hoc(Child)
function App() {
return (
<div className="App">
<h3>App</h3>
<HocChild />
</div>
)
}
function Child() {
return <div>Child</div>
}
// 给组件添加border
function hoc(Comp) {
return props => (
<div style={{ border: '1px solid #000' }}>
<Comp {...props} />
</div>
)
}
export default App也可以使用ES7语法装饰器:
yarn add customize-cra @babel/plugin-proposal-decorators
yarn add react-app-rewired -D # react-app-rewired 和 customize-cra 是配置 CRA 的插件配置package.json:
{
"scripts": {
"start": "react-app-rewired start", // 之前是react-scripts start/build/..
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
}
}配置 config-overrides.js:
const { addDecoratorsLegacy, override } = require('customize-cra')
module.exports = override(
addDecoratorsLegacy()
)如果VSCode报错,设置:
"javascript.implicitProjectConfig.experimentalDecorators": true组件:
import React from "react"
function App() {
return (
<div className="App">
<h3>App</h3>
<Child />
</div>
)
}
@hoc
class Child extends React.Component {
render() {
return (
<div>Child</div>
)
}
}
// 给组件添加border
function hoc(Comp) {
return props => (
<div style={{ border: '1px solid #000' }}>
<Comp {...props} />
</div>
)
}
export default App自己实现 antd 表单校验
import React from "react"
@formHoc
class Form extends React.Component {
render() {
const { getFieldDecorator, getFieldsValue, getFieldValue, validateFields } = this.props
console.log(this.props)
return (
<div>
<h3>表单</h3>
{getFieldDecorator('name', {
required: true
})(
<input type="text" placeholder="username" />
)}
{getFieldDecorator('password')(
<input type="password" placeholder="password" />
)}
<button onClick={() => validateFields((err, values) => {
if (err) console.log('err', err)
else console.log('values', values)
})}>提交</button>
</div>
)
}
}
// 表单装饰器
function formHoc(Comp) {
return class extends React.Component {
state = {}
options = {}
getFieldDecorator = (field, option) => {
this.options[field] = option
return InputComp => {
// 这个地方不能用<InputComp />,因为高阶组件的基本原则是:不改变原组件
return React.cloneElement(InputComp, {
value: this.state[field] || '',
onChange: (e) => this.setState({ [field]: e.target.value })
})
}
}
getFieldsValue = () => {
return this.state
}
getFieldValue = (name) => {
return this.state[name]
}
// 校验字段是否合法
validateFields = (callback) => {
const err = {}
for (let k in this.options) {
if (this.options[k] && this.options[k].required) {
if (this.state[k] === undefined) err[k] = `${k} is required`
}
}
if (Object.keys(err).length === 0) {
callback(undefined, this.state)
} else {
callback(err)
}
}
render() {
return (
<Comp
{...this.props}
getFieldDecorator={this.getFieldDecorator}
getFieldsValue={this.getFieldsValue}
getFieldValue={this.getFieldValue}
validateFields={this.validateFields}
/>
)
}
}
}
export default Form自己实现 Dialog
使用ReactDOM.createPortal来挂载到 document.body 上。
Context 使用和 Redux 实现
context
老版本写法
{% raw %}
import React from "react"
// context
const defaultValue = undefined
const themeContext = React.createContext(defaultValue)
const ThemeProvider = themeContext.Provider
class App extends React.Component {
state = {
theme: {
defaultColor: 'red'
}
}
render() {
return (
<ThemeProvider value={{ theme: this.state.theme, setTheme: () => { } }}>
<h3>App</h3>
<Page />
</ThemeProvider>
)
}
}
class Page extends React.Component {
// 这是一个固定的静态属性,设置后,实例可以拿到this.context
static contextType = themeContext
render() {
console.log(this.context)
return (
<div>page</div>
)
}
}
export default App{% endraw %}
这么写的问题是:
- 只能用于class组件
- 一个组件只能使用一个context(虽然一般情况下也不会使用多个context)
新版本写法
{% raw %}
import React from "react"
// context
const defaultValue = undefined
const themeContext = React.createContext(defaultValue)
const ThemeProvider = themeContext.Provider
const ThemeConsumer = themeContext.Consumer
// createContext() 的参数是默认 context,它用来:
/// 在没有写 Provider 而直接使用 Consumer 的情况下,作为 ctx 的值
/// 如果写了 Provider,这个值是无效的,因为<Provider />需要传 `value` 属性
// context 2
const userContext = React.createContext()
class App extends React.Component {
state = {
theme: {
defaultColor: 'red'
},
user: {
name: 'root'
}
}
render() {
return (
<ThemeProvider value={{ theme: this.state.theme, setTheme: () => { } }}>
<h3>App</h3>
<userContext.Provider value={{ user: this.state.user }}>
<userContext.Consumer>
{userCtx => (
<ThemeConsumer>
{themeCtx => (
<Page userCtx={userCtx} themeCtx={themeCtx} />
)}
</ThemeConsumer>
)}
</userContext.Consumer>
</userContext.Provider>
</ThemeProvider>
)
}
}
// 不需要设置class组件的contextType,而且可以应用多个context
function Page(props) {
console.log(props)
return (
<div>page</div>
)
}
export default App{% endraw %}
手写redux
基本实现:
// redux
export function createStore(reducer, enhancer) {
// 如果应用了中间件
if (enhancer) {
return enhancer(createStore)(reducer)
}
let state = undefined
let listeners = []
const getState = () => state
const dispatch = action => {
state = reducer(state, action)
listeners.forEach(item => item(state))
}
const subscribe = (listener) => {
listeners.push(listener)
}
// 赋初值。注意这里的type不要跟用户定义的重复了,才能走到 case: default 那一步
dispatch({ type: '@INIT/REDUX' })
return {
getState,
dispatch,
subscribe
}
}
export function combineReducers() { }
// 返回值是 enhancer
// 使用时,应该先传入logger,再传入thunk
export function applyMiddleware(...middlewares) {
const enhancer = (createStore) => (reducer) => {
const store = createStore(reducer)
let dispatch = store.dispatch
// 这里的middleApi在下面的代码中没有被使用
const middleApi = {
getState: store.getState,
dispatch
}
const chain = middlewares.map(item => item(middleApi))
dispatch = compose(...chain)(dispatch)
return {
...store,
dispatch
}
}
return enhancer
}
export function reduxThunk(middleApi) {
return (dispatch) => (action) => {
if (typeof action === 'function') {
return action(dispatch)
}
return dispatch(action)
}
}
export function reduxLogger(middleApi) {
return dispatch => action => {
console.log(action.type + '执行了')
return dispatch(action)
}
}
function compose(...funcs) {
if (funcs.length === 0) return arg => arg
if (funcs.length === 1) return funcs[0]
return (arg) => funcs.reduce((prev, cur) => {
return cur(prev)
}, arg)
}关于compose
redux 中的 compose 是这么写的:
export default function compose(...funcs) {
if (funcs.length === 0) {
return (arg) => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}两个疑问:
- reduce返回函数而不是值的好处是什么
- args为啥要解构;真的有多个参数的时候,compose可以处理吗
下面是朴素的写法:
let a = {
name: 'Jack'
}
const f1 = (obj) => ({ ...obj, sex: 'male' })
const f2 = (obj) => ({ ...obj, sex: 'female' })
const f3 = (obj) => ({ ...obj, age: 18 })
function compose(...funcs) {
if (funcs.length === 0) return arg => arg
if (funcs.length === 1) return funcs[0]
return (arg) => funcs.reduce((prev, cur) => {
return cur(prev)
}, arg)
}
a = compose(f1, f2, f3)(a)
// 结果:{ name: 'Jack', sex: 'female', age: 18 }
console.log(a)react-redux 实现和 react-router 动态路由
react-redux
使用
page.js:
import React from "react"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
@connect(
// 第一个参数:mapStateToProps
(state) => ({ count: state.count }),
// 第二个参数:mapDispatchToProps: 可以是Object或function
{
add: () => ({ type: 'ADD' })
}
/**
* 下面是function写法:
* @code:
*
(dispatch, ownProps) => {
let methods = {
add: () => ({ type: 'ADD' }),
minus: () => ({ type: 'minus' })
}
// bindActionCreators 把所有的函数返回值作为 dispatch() 的参数
methods = bindActionCreators(methods, dispatch)
return {
dispatch,
...methods
}
}
*/
// 第三个参数:它拿到前两个参数的结果,计算完毕后,返回给组件;几乎不会用
/**
* @code:
*
(stateProps, dispatchProps, ownProps) => {
return { ...stateProps, ...dispatchProps }
}
*/
)
class Page extends React.Component {
render() {
console.log('this.props: ', this.props)
return (
<div>
<p>{this.props.count}</p>
<button onClick={() => this.props.add()}>加1</button>
</div>
)
}
}
export default PageApp.js:
import React from "react"
import { Provider } from "react-redux"
import { createStore } from "redux"
import Page from './page'
const initState = { count: 1 }
function reducer(state = initState, action) {
switch (action.type) {
case 'ADD': {
return {
...state,
count: state.count + 1
}
}
case 'MINUS': {
return {
...state,
count: state.count - 1
}
}
default: {
return state
}
}
}
const store = createStore(reducer)
function App() {
return (
<Provider store={store}>
<h3>App</h3>
<Page />
</Provider>
)
}
export default App实现 connect 和 Provider
import React from "react"
const reduxContext = React.createContext()
// HOC
export function connect(mapStatetoProps, mapDispatchToProps, mergeProps) {
return (Comp) => {
return class extends React.Component {
static contextType = reduxContext
state = {
reduxProps: {}
}
componentDidMount() {
this.update()
// connect 使得:组件实际上用的是 this.state.reduxProps
// 当 store 变化时,需要同步到 this.state.reduxProps
this.context.subscribe(() => {
this.update()
})
}
update = () => {
const { getState, dispatch } = this.context
// mapStateToProps
let stateProps = mapStatetoProps(getState())
// mapDispatchToProps
let dispatchProps = { dispatch }
if (typeof mapDispatchToProps === 'object') {
dispatchProps = bindActionCreators(mapDispatchToProps, dispatch)
} else if (typeof mapDispatchToProps === 'function') {
dispatchProps = mapDispatchToProps(dispatch)
}
this.setState({
reduxProps: {
...stateProps,
...dispatchProps
}
})
}
render() {
return <Comp {...this.state.reduxProps} />
}
}
}
}
export class Provider extends React.Component {
render() {
const { store, children } = this.props
return (
<reduxContext.Provider value={store}>
{children}
</reduxContext.Provider>
)
}
}
// () => ({ }) 转换为 () => func({ })
function bindActionCreators(creators, func) {
let ret = {}
for (let k in creators) {
ret[k] = () => func(creators[k]())
}
return ret
}react-router
动态路由的使用:
import react from 'react'
import { Route, Link, Router, Switch, Redirect } from 'react-router'
// 假设 SearchPage 匹配了 /search/:id
function App() {
return (
<div>
<Router>
<Link to="/login">登录</Link>
<Link to="/admin">管理员</Link>
<Link to="/search/888">搜索id为888</Link>
<Switch>
<Route path="/login" component={Login} />
<Route path="/admin" component={Admin} />
<Route path="/search/:id" component={SearchPage} />
</Switch>
</Router>
</div>
)
}
function Login() {
return <div>login</div>
}
// 路由拦截
function Admin(props) {
const { isLogin } = props
return isLogin ? (
<AdminPage />
) : (
<Redirect
to={{ pathname: '/login', state: { fromPath: '/admin' /*当前页面地址*/ }}}
/>
)
}
function SearchPage(props) {
const { id } = props.match.params
return (
<div>
<h3>{id}的页面</h3>
<Link to={`/search/${id}/detail`}>查看详情</Link>
{/* 外层有 <Router>,这儿不用写了。注意Route的冒号 */}
<Route path={`/search/:${id}/detail`} component={DetailPage} />
</div>
)
}HashRouter 就是带“#”,它不支持history.state 和 history.key。
BrowserRouter需要服务器配置,将路由交由前端自己处理。
MemoryRouter将路由保存在内存中,不读取、不写入地址栏。通常在React Native使用。
它们都有 basename (如 basename = /prefix) 属性:当路由匹配时,url自动加上 basename。
react-router 实现
react-router实现
import React from "react"
// createBrowserHistory 实际上创建了一个window.history,使用的就是window.history的API,同时封装了其他的方法。
// 这个库平时是随着react-router-dom安装的
import { createBrowserHistory } from 'history'
// context
const routerContext = React.createContext()
export class BrowserRouter extends React.Component {
constructor(props) {
super(props)
const history = createBrowserHistory()
this.state = {
history,
location: history.location
}
this.unlisten = history.listen((newHistory) => {
this.setState({ location: newHistory.location })
})
}
compomentWillUnmount() {
if (this.unlisten) this.unlisten()
}
render() {
return (
<routerContext.Provider value={this.state}>
{this.props.children}
</routerContext.Provider>
)
}
}
export class Link extends React.Component {
handleClick = (e, ctx) => {
e.preventDefault()
ctx.history.push(this.props.to)
}
render() {
const { children } = this.props
return (
<routerContext.Consumer>
{ctx => (
<a onClick={e => this.handleClick(e, ctx)}>{children}</a>
)}
</routerContext.Consumer>
)
}
}
export class Route extends React.Component {
render() {
return (
<routerContext.Consumer>
{ctx => {
// 有很多功能就不做了:
// 1. 判断是否match,children如果有就显示,以及children、render、component的优先级等
// 2. 实际上component props 还包括location、match,其中match的正则可以判断动态路由
// 3. 嵌套路由
// 4. exact,其实就是正则判断
// 5. ..
const { path, component, render, children } = this.props
const compProps = {
...ctx
}
// 使用不精确匹配,以测试Switch
const isMatch = path.includes(ctx.location.pathname)
return isMatch ? React.createElement(component, this.props) : null
}}
</routerContext.Consumer>
)
}
}
// 这个组件
export class Switch extends React.Component {
render() {
return (
<routerContext.Consumer>
{ctx => {
const { children } = this.props
let match = null, element
React.Children.forEach(children, item => {
if (match === null) {
// 这儿的判断很粗糙,但知道意思就行
if (item.props.path === ctx.location.pathname && React.isValidElement(item)) {
match = item
}
}
})
return match
}}
</routerContext.Consumer>
)
}
}路由嵌套复习
function L() {
return (
<Router>
<Route component={Layout}>
<Switch>
<Route path="/" component={Index} />
<Route path="/user" component={User} />
</Switch>
</Route>
</Router>
)
}redux-saga的使用
takeEvery用来处理saga事件的监听
function* watchFetchProducts() {
yield takeEvery('PRODUCTS_REQUESTED', fetchProducts)
}call 用来调用函数
put 用来发送 redux 事件,相当于 store.dispatch
事件循环
事件循环(Event Loop)
主线程(main thread):是 JS 发生的地方,是渲染发生的地方,是 DOM 存在的地方
事件循环(Event Loop):主线程的运行机制就是事件循环。
setTimeout不是V8定义的,是浏览器实现的 web api。当 setTimeout 执行时,它的 callback 会被推入到 task queue,当时间到达且 JS call stack 为空时,callback 会被推入到 JS call stack。
如果 call stack 有同步代码花费很长时间,如 while: true,它将阻塞视图渲染(rendering pipeline).
如果 microtask queue 有代码花费很长时间,它同样会阻塞视图渲染。
如果 task queue 有代码花费很长时间,你仍然可以正常查看或操作视图,比如选择文本。
事件的循环机制:call stack 清空;执行 microtask queue 直至清空,执行 task queue 中一个任务,然后事件循环查看 microtask queue,然后…
宏任务和微任务:
setTimeout(() => {
console.log('Timeout 1')
Promise.resolve().then(() => console.log('Microtask 1'))
})
setTimeout(() => {
console.log('Timeout 2')
})
setTimeout(() => {
console.log('Timeout 3')
})输出:“Timeout 1; Microtask 1; Timeout 2; Timeout 3”
通常情况下,DOM 操作会进行合并再显示到 DOM 上。所以要实现 DOM 动画可以这么写:
const btn = document.getElementById('btn')
btn.style.transition = "transform 1s ease-in-out"
btn.style.transform = "translateX(1000px)"
requestAnimationFrame(() => {
requestAnimationFrame(() => {
btn.style.transform = "translateX(500px)"
})
})getComputedStyle 会使浏览器被迫尽早计算元素样式,所以也可以实现 DOM 的重新渲染,但是很消耗性能。
事件触发的对象不同,会有不同的结果:
const box = document.getElementById('box')
const btn = document.getElementById('btn')
btn.addEventListener('click', () => {
Promise.resolve().then(() => console.log('Microtask 1'))
console.log('Listener 1')
})
btn.addEventListener('click', () => {
Promise.resolve().then(() => console.log('Microtask 2'))
console.log('Listener 2')
})
// btn.click()如果代码由 DOM 操作触发,结果是 “Microtask 1; Listener 1; Microtask 2; Listener 2”
如果代码由 btn.click 触发,结果是 “Microtask 1; Microtask 2; Listener 1; Listener 2”
因为对 DOM 操作而言,一个事件回调就是一个调用栈;而 btn.click 执行时,它自己会产生一个调用栈,btn.click 没有执行完毕时,所有的回调会继续执行。调用栈被清空时,microtask 才开始执行。
async 和 await:
function f() {
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
// async1 等价于下面这个函数
// function async1() {
// return new Promise(() => {
// async2().then(() => {
// console.log('async1 end')
// })
// })
// }
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function () {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function () {
console.log('promise1')
})
.then(function () {
console.log('promise2')
})
console.log('script end')
}
f()输出:“script start; async2 end; Promise; script end; async1 end; promise1; promise2; setTimeout”
手写Promise
注意异步部分使用MutationObserver,别用宏任务。
React 源码
实现简单的react:
// React 17 使用了新版的自动引入 react 的方法,这里需要用旧版的
/** @jsxRuntime classic */
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object" ? child : createTextElement(child)
)
}
}
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: []
}
}
}
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
updateDom(dom, {}, fiber.props)
return dom
}
const isEvent = key => key.startsWith("on")
const isProperty = key => key !== "children" && !isEvent(key)
const isNew = (prev, next) => key => prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)
function updateDom(dom, prevProps, nextProps) {
//Remove old or changed event listeners
Object.keys(prevProps)
.filter(isEvent)
.filter(key => !(key in nextProps) || isNew(prevProps, nextProps)(key))
.forEach(name => {
const eventType = name.toLowerCase().substring(2)
dom.removeEventListener(eventType, prevProps[name])
})
// Remove old properties
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// Set new or changed properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
// Add event listeners
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name.toLowerCase().substring(2)
dom.addEventListener(eventType, nextProps[name])
})
}
function commitRoot() {
deletions.forEach(commitWork)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
function commitWork(fiber) {
if (!fiber) {
return
}
let domParentFiber = fiber.parent
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom)
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props)
} else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
},
alternate: currentRoot
}
deletions = []
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let currentRoot = null
let wipRoot = null
let deletions = null
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
shouldYield = deadline.timeRemaining() < 1
}
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork(fiber) {
const isFunctionComponent = fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
let wipFiber = null
let hookIndex = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
queue: []
}
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
hook.state = action(hook.state)
})
const setState = action => {
hook.queue.push(action)
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot
}
nextUnitOfWork = wipRoot
deletions = []
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
function reconcileChildren(wipFiber, elements) {
let index = 0
let oldFiber = wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null
while (index < elements.length || oldFiber != null) {
const element = elements[index]
let newFiber = null
const sameType = oldFiber && element && element.type == oldFiber.type
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE"
}
}
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT"
}
}
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
if (oldFiber) {
oldFiber = oldFiber.sibling
}
if (index === 0) {
wipFiber.child = newFiber
} else if (element) {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
const Didact = {
createElement,
render,
useState
}
// 标记使用自定义的 react
/** @jsx Didact.createElement */
function Counter() {
const [state, setState] = Didact.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)} style="user-select: none">
Count: {state}
</h1>
)
}
const element = <Counter />
const container = document.getElementById("root")
Didact.render(element, container)
