Home
avatar

dxk

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.js

store/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

  1. Provider
  2. 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-routerreact-router-domreact-router-native

常用组件:

  1. 是跳转的链接
  2. 是匹配的页面

  3. 是最外层

  4. 配合 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一直重新挂载。

有3个重要属性:component、children 和 render。

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 规则:

  1. 只能在函数最外层调用Hook,不能在循环、条件判断或者子函数中调用
  2. 只能在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 %}

这么写的问题是:

  1. 只能用于class组件
  2. 一个组件只能使用一个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)))
}

两个疑问

  1. reduce返回函数而不是值的好处是什么
  2. 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 Page

App.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.statehistory.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的使用

https://redux-saga-in-chinese.js.org/

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:

https://pomb.us/build-your-own-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)
React