Giter Club home page Giter Club logo

javascript's People

Contributors

zouxiaomingya avatar

Stargazers

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

Watchers

 avatar

javascript's Issues

如何,何时使用 useContext

开头

一个项目,一个复杂的逻辑,我觉得状态管理显得尤为的重要,状态管理的好不好,直接体现了一个项目的逻辑性、可读性、维护性等是否清晰,易读,和高效。

从最早的类组件使用 this.state, this.setState 去管理状态,到 redux , subscribe, dispatch 的发布订阅,redux 的使用就面临重复和沉重的的 reducer,让我俨然变成了 Ctrl CV 工程师。于是后面接触 dva,它是一个基于 reduxredux-saga 的数据流方案。通过 model 来分片管理全局状态,使用 connect 方法去给需要的深层次的组件传递状态。

到后面 react hooks 出来之后,业界也出了很多自身管理状态的,基于 hooks 封装,各个模块都有一个基于自己 hooks 的状态 store。确实很好的解决了函数组件的状态管理,和模块自身内部的状态管理,但是还是解决不了在全局组件中,层层传递的状态依赖让结构变得复杂,繁琐的问题。不用任何的管理工具我们如何做到跨组件通信?

为什么不用?

不是说我们不去用 dva 这样的管理工具?我并不是说 dva 不好用,而是我觉得有时候没必要用。我觉得他太重了。

学到什么?

读完这边文章,即使你觉得我的管理方式不好,你也可以学习和了解到 useMemo, useContext,useImmer等。

react context

Context-React 官网介绍

// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。
  // React 会往上找到最近的 theme Provider,然后使用它的值。
  // 在这个例子中,当前的 theme 值为 “dark”。
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

createContext 实现跨组件通信的大致过程

const MyContext = React.createContext(defaultValue);

<MyContext.Provider value={/* 某个值 */}>
    <App>
      ... 多层组件嵌套内有一个 Goods 组件
            <Goods />
  </App>  
</MyContext.Provider >


// 某个 子子子子子组件 Goods
<MyContext.Consumer>
  {value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>

具体实际案例

app.js

import {ThemeContext, themes} from './theme-context';
import ThemeTogglerButton from './theme-toggler-button';

class App extends React.Component {
  constructor(props) {
    super(props);

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };

    // State 也包含了更新函数,因此它会被传递进 context provider。
    this.state = {
      theme: themes.light,
      toggleTheme: this.toggleTheme,
    };
  }

  render() {
    // 整个 state 都被传递进 provider
    return (
      <ThemeContext.Provider value={this.state}>
        <Content />
      </ThemeContext.Provider>
    );
  }
}

function Content() {
  return (
    <div>
      <ThemeTogglerButton />
    </div>
  );
}

ReactDOM.render(<App />, document.root);
// Theme context,默认的 theme 是 “light” 值
const ThemeContext = React.createContext('light');

// 用户登录 context
const UserContext = React.createContext({
  name: 'Guest',
});

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;

    // 提供初始 context 值的 App 组件
    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}

function Layout() {
  return (
    <div>
      <Sidebar />
      <Content />
    </div>
  );
}

// 一个组件可能会消费多个 context
function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

封装自己的跨组件管理方式

./connect.js 文件

封装 connect 方法

使用 connect 也是基于 react-redux **,把它封装为一个方法。调用 connect 方法返回的是一个高阶组件。并且 connect 方法中支持传入一个函数,来过滤,筛选子组件需要的状态,也便于维护 重新 render 等

import React, { createContext } from 'react';
import { useImmer } from 'use-immer';
// useImmer 文章末尾有介绍推荐

const ctx = createContext();
const { Consumer, Provider } = ctx

const useModel = (initialState) => {
  const [state, setState] = useImmer(initialState);
  return [
    state,
    setState
  ];
}

const createProvider = () => {
  function WrapProvider(props) {
    const { children, value } = props;
    const [_state, _dispatch] = useModel(value)
    return (
      <Provider value={{
        _state,
        _dispatch,
      }}>
        {children}
      </Provider>
    )
  }
  return WrapProvider
}

export const connect = fn => ComponentUi => () => {
  return (
    <Consumer>
      {
        state => {
          const {_state, _dispatch} = state
          const selectState = typeof fn === 'function' ? fn(_state) : _state;
          return <ComponentUi _state={selectState} _dispatch={_dispatch} />
        }
      }
    </Consumer>
  )
};

export default createProvider;

使用方式

import React from 'react';
import Header from './layout/Header.jsx';
import Footer from './layout/Footer.jsx';
import createProvider from './connect';

const Provider = createProvider()

const initValue = { user: 'xiaoming', age: 12 }
function App() {
  return (
    <Provider value={initValue}>
        <Header />
        <Footer />
    </Provider>

  )
}
export default App;

Header.jsx

import React from 'react';
import { Select } from 'antd';
import { connect } from '../connect';
const { Option } = Select;

function Head({ _state: { user, age }, _dispatch }) {
  return (
    <div className="logo" >
      <Select defaultValue={user} value={user} onChange={(value) => {
        _dispatch(draft => {
          draft.user = value
        })
      }}>
        <Option value='xiaoming'>小明</Option>
        <Option value='xiaohong'>小红</Option>
      </Select>
      <span>年龄{age}</span>
    </div>
  )
}

export default connect()(Head);

Footer.jsx

import React, { Fragment } from 'react';
import { Select } from 'antd';
import { connect } from '../../connect';

const { Option } = Select;
function Footer({ _state, _dispatch }) {
  const { user, age } = _state;
  return (
    <Fragment>
      <p style={{marginTop: 40}}>用户:{user}</p>
      <p>年龄{age}</p>
      <div>
        <span>改变用户:</span>
        <Select
          defaultValue={user}
          value={user}
          onChange={(value) => {
            _dispatch(draft => {
              draft.user = value
            })
          }}>
          <Option value='xiaoming'>小明</Option>
          <Option value='xiaohong'>小红</Option>
        </Select></div>
      <div>
        <span>改变年龄:</span>
        <input onChange={(e) => {
          // 这里使用 persist 原因可以看文章末尾推荐
          e.persist();
          _dispatch(draft => {
            draft.age = e.target.value
          })
        }} />
      </div>
    </Fragment>
  )
}

export default connect()(Footer);

使用 useContext

我们都知道 react 16.8 以后也出了 useContext 那么我们可以通过使用 useContext 来优化 connect 方法

// 未使用 useContext
export const connect = (fn) => (ComponentUi) => () => {
  const state = useContext(ctx)
  console.log(state);
  return (
    <Consumer>
      {
        state => {
          const { _state, _dispatch } = state
          const selectState = typeof fn === 'function' ? fn(_state) : _state;
          return <ComponentUi _state={selectState} _dispatch={_dispatch} />
        }
      }
    </Consumer>
  )
};

// 使用 useContext
export const connect = fn => ComponentUi => () => {
  const { _state, _dispatch } = useContext(ctx);
  const selectState = typeof fn === 'function' ? fn(_state) : _state;
  return <ComponentUi _state={selectState} _dispatch={_dispatch} />;
};

注意: 调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以通过文章末尾推荐的不必要重新 render 开销大的组件去了解如何优化。

最后

github地址

4步代码跑起来

git clone https://github.com/zouxiaomingya/blog
cd blog
npm i
npm start

全文章,如有错误或不严谨的地方,请务必给予指正,谢谢!

参考:

Hooks 深入剖析

useState 模拟解析

useState 使用

通常我们这样来使用 useState 方法

function App() {
  const [num, setNum] = useState(0);
  const add = () => {
    setNum(num + 1);
  };
  return (
    <div>
      <p>数字: {num}</p>
      <button onClick={add}> +1 </button>
    </div>
  );
}

useState 的使用过程,我们先模拟一个大概的函数

function useState(initialValue) {
  var value = initialValue
  function setState(newVal) {	
    value = newVal
  }
  return [value, setState]
}

这个代码有一个问题,在执行 useState 的时候每次都会 var _val = initialValue,初始化数据;

于是我们可以用闭包的形式来保存状态。

const MyReact = (function() {
   // 定义一个 value 保存在该模块的全局中
  let value
  return {
    useState(initialValue) {
      value = value || initialValue 
      function setState(newVal) {
        value = newVal
      }
      return [value, setState]
    }
  }
})()

这样在每次执行的时候,就能够通过闭包的形式 来保存 value。

不过这个还是不符合 react 中的 useState。因为在实际操作中会出现多次调用,如下。

function App() {
  const [name, setName] = useState('Kevin');
  const [age, setAge] = useState(0);
  const handleName = () => {
    setNum('Dom');
  };
  const handleAge = () => {
    setAge(age + 1);
  };
  return (
    <div>
      <p>姓名: {name}</p>
      <button onClick={handleName}> 改名字 </button>
 	  <p>年龄: {age}</p>
      <button onClick={handleAge}> 加一岁 </button>
    </div>
  );
}

因此我们需要在改变 useState 储存状态的方式

useState 模拟实现

const MyReact = (function() {
  // 开辟一个储存 hooks 的空间
  let hooks = []; 
  // 指针从 0 开始
  let currentHook = 0 
  return {
    // 伪代码 解释重新渲染的时候 会初始化 currentHook
    render(Component) {
      const Comp = Component()
      Comp.render()
      currentHook = 0 // 重新渲染时候改变 hooks 指针
      return Comp
    },      
    useState(initialValue) {
      hooks[currentHook] = hooks[currentHook] || initialValue
      const setStateHookIndex = currentHook
      // 这里我们暂且默认 setState 方式第一个参数不传 函数,直接传状态
      const setState = newState => (hooks[setStateHookIndex] = newState)
      return [hooks[currentHook++], setState]
    }
  }
})()

1565939661177

因此当重新渲染 App 的时候,再次执行 useState 的时候传入的参数 kevin , 0 也就不会去使用,而是直接拿之前 hooks 存储好的值。

hooks 规则

官网 hoos 规则中明确的提出 hooks 不要再循环,条件或嵌套函数中使用。

1565941239096

为什么不可以?

我们来看下

下面这样一段代码。执行 useState 重新渲染,和初始化渲染 顺序不一样就会出现如下问题

如果了解了上面 useState 模拟写法的存储方式,那么这个问题的原因就迎刃而解了。

1565940865445

useEffect 解析

useEffect 使用

初始化会 打印一次 ‘useEffect_execute’, 改变年龄重新render,会再打印, 改变名字重新 render, 不会打印。因为依赖数组里面就监听了 age 的值

import React, { useState, useEffect } from 'react';

function App() {
  const [name, setName] = useState('Kevin');
  const [age, setAge] = useState(0);
  const handleName = () => {
    setName('Don');
  };
  const handleAge = () => {
    setAge(age + 1);
  };
  useEffect(()=>{
    console.log('useEffect_execute')
  }, [age])
  return (
    <div>
      <p>姓名: {name}</p>
      <button onClick={handleName}> 改名字 </button>
      <p>年龄: {age}</p>
      <button onClick={handleAge}> 加一岁 </button>
    </div>
  );
}
export default App;

useEffect 的模拟实现

const MyReact = (function() {
  // 开辟一个储存 hooks 的空间
  let hooks = []; 
  // 指针从 0 开始
  let currentHook = 0 
  // 定义个模块全局的 useEffect 依赖
  let deps;
  return {
    // 伪代码 解释重新渲染的时候 会初始化 currentHook
    render(Component) {
      const Comp = Component()
      Comp.render()
      currentHook = 0 // 重新渲染时候改变 hooks 指针
      return Comp
    },      
    useState(initialValue) {
      hooks[currentHook] = hooks[currentHook] || initialValue
      const setStateHookIndex = currentHook
      // 这里我们暂且默认 setState 方式第一个参数不传 函数,直接传状态
      const setState = newState => (hooks[setStateHookIndex] = newState)
      return [hooks[currentHook++], setState]
    }
    useEffect(callback, depArray) {
      const hasNoDeps = !depArray
      // 如果没有依赖,说明是第一次渲染,或者是没有传入依赖参数,那么就 为 true
      // 有依赖 使用 every 遍历依赖的状态是否变化, 变化就会 true
      const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true
      // 如果有 依赖, 并且依赖改变
      if (hasNoDeps || hasChangedDeps) {
        // 执行 
        callback()
        // 更新依赖
        deps = depArray
      }
    },
        
  }
})()

useEffect 注意事项

依赖项要真实

依赖需要想清楚。

刚开始使用 useEffect 的时候,我只有想重新触发 useEffect 的时候才会去设置依赖

那么也就会出现如下的问题。

希望的效果是界面中一秒增加一岁

import React, { useState, useEffect } from 'react';

function App() {
  const [name, setName] = useState('Kevin');
  const [age, setAge] = useState(0);
  const handleName = () => {
    setName('Don');
  };
  const handleAge = () => {
    setAge(age + 1);
  };
  useEffect(() => {
    setInterval(() => {
      setAge(age + 1);
      console.log(age)
    }, 1000);
  }, []);
  return (
    <div>
      <p>姓名: {name}</p>
      <button onClick={handleName}> 改名字 </button>
      <p>年龄: {age}</p>
      <button onClick={handleAge}> 加一岁 </button>
    </div>
  );
}
export default App;

其实你会发现 这里界面就增加了 一次 年龄。究其原因:

**在第一次渲染中,age0。因此,setAge(age+ 1)在第一次渲染中等价于setAge(0 + 1)。然而我设置了0依赖为空数组,那么之后的 useEffect 不会再重新运行,它后面每一秒都会调用setAge(0 + 1) **

也就是当我们需要 依赖 age 的时候我们 就必须再 依赖数组中去记录他的依赖。这样useEffect 才会正常的给我们去运行。

所以我们想要每秒都递增的话有两种方法

方法一:

真真切切的把你所依赖的状态填写到 数组中

  // 通过监听 age 的变化。来重新执行 useEffect 内的函数
  // 因此这里也就需要记录定时器,当卸载的时候我们去清空定时器,防止多个定时器重新触发
  useEffect(() => {
    const id = setInterval(() => {
      setAge(age + 1);
    }, 1000);
    return () => {
      clearInterval(id)
    };
  }, [age]);

方法二

useState 的参数传入 一个方法。

注:上面我们模拟的 useState 并没有做这个处理 后面我会讲解源码中去解析。

useEffect(() => {
    setInterval(() => {
      setAge(age => age + 1);
    }, 1000);
  }, []);

useEffect 只运行了一次,通过 useState 传入函数的方式它不再需要知道当前的age值。因为 React render 的时候它会帮我们处理

这正是setAge(age => age + 1)做的事情。再重新渲染的时候他会帮我们执行这个方法,并且传入最新的状态。

所以我们做到了去时刻改变状态,但是依赖中却不用写这个依赖,因为我们将原本的使用到的依赖移除了。(这句话表达感觉不到位)

接口无限请求问题

刚开始使用 useEffect 的我,在接口请求的时候常常会这样去写代码。

props 里面有 页码,通过切换页码,希望监听页码的变化来重新去请求数据

// 以下是伪代码 
// 这里用 dva 发送请求来模拟

import React, { useState, useEffect } from 'react';
import { connect } from 'dva';

function App(props) {
  const { goods, dispatch, page } = props;
  useEffect(() => {
    // 页面完成去发情请求
   dispatch({
      type: '/goods/list',
      payload: {page, pageSize:10},
    });
    // xxxx 
  }, [props]);
  return (
    <div>
      <p>商品: {goods}</p>
	 <button>点击切下一页</button>
    </div>
  );
}
export default connect(({ goods }) => ({
  goods,
}))(App);

然后得意洋洋的刷新界面,发现 Network 中疯狂循环的请求接口,导致页面的卡死。

究其原因是因为在依赖中,我们通过接口改变了状态 props 的更新, 导致重新渲染组件,导致会重新执行 useEffect 里面的方法,方法执行完成之后 props 的更新, 导致重新渲染组件,依赖项目是对象,引用类型发现不相等,又去执行 useEffect 里面的方法,又重新渲染,然后又对比,又不相等, 又执行。因此产生了无限循环。

Hooks 源码解析

该源码位置: react/packages/react-reconciler/src/ReactFiberHooks.js

const Dispatcher={
  useReducer: mountReducer,
  useState: mountState,
  // xxx 省略其他的方法
}

mountState 源码

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
    /*
    mountWorkInProgressHook 方法 返回初始化对象
    {
        memoizedState: null,
        baseState: null, 
        queue: null,
        baseUpdate: null,
        next: null,
  	}
    */
  const hook = mountWorkInProgressHook();
 // 如果传入的是函数 直接执行,所以第一次这个参数是 undefined
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });

	/*
	定义 dispatch 相当于
	const dispatch = queue.dispatch =
	dispatchAction.bind(null,currentlyRenderingFiber,queue);
	*/ 
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    // Flow doesn't know this is non-null, but we do.
    ((currentlyRenderingFiber: any): Fiber),
    queue,
  ): any));

 // 可以看到这个dispatch就是dispatchAction绑定了对应的 currentlyRenderingFiber 和 queue。最后return:
  return [hook.memoizedState, dispatch];
}

dispatchAction 源码

function dispatchAction<A>(fiber: Fiber, queue: UpdateQueue<A>, action: A) {
  //... 省略验证的代码
  const alternate = fiber.alternate;
    /*
    这其实就是判断这个更新是否是在渲染过程中产生的,currentlyRenderingFiber只有在FunctionalComponent更新的过程中才会被设置,在离开更新的时候设置为null,所以只要存在并更产生更新的Fiber相等,说明这个更新是在当前渲染中产生的,则这是一次reRender。
所有更新过程中产生的更新记录在renderPhaseUpdates这个Map上,以每个Hook的queue为key。
对于不是更新过程中产生的更新,则直接在queue上执行操作就行了,注意在最后会发起一次scheduleWork的调度。
    */
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    didScheduleRenderPhaseUpdate = true;
    const update: Update<A> = {
      expirationTime: renderExpirationTime,
      action,
      next: null,
    };
    if (renderPhaseUpdates === null) {
      renderPhaseUpdates = new Map();
    }
    const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
    if (firstRenderPhaseUpdate === undefined) {
      renderPhaseUpdates.set(queue, update);
    } else {
      // Append the update to the end of the list.
      let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
      while (lastRenderPhaseUpdate.next !== null) {
        lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
      }
      lastRenderPhaseUpdate.next = update;
    }
  } else {
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);
    const update: Update<A> = {
      expirationTime,
      action,
      next: null,
    };
    flushPassiveEffects();
    // Append the update to the end of the list.
    const last = queue.last;
    if (last === null) {
      // This is the first update. Create a circular list.
      update.next = update;
    } else {
      const first = last.next;
      if (first !== null) {
        // Still circular.
        update.next = first;
      }
      last.next = update;
    }
    queue.last = update;
    scheduleWork(fiber, expirationTime);
  }
}

mountReducer 源码

多勒第三个参数,是函数执行,默认初始状态 undefined

其他的和 上面的 mountState 大同小异

function mountReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = mountWorkInProgressHook();
  let initialState;
  if (init !== undefined) {
    initialState = init(initialArg);
  } else {
    initialState = ((initialArg: any): S);
  }
	// 其他和 useState 一样
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
    null,
    // Flow doesn't know this is non-null, but we do.
    ((currentlyRenderingFiber: any): Fiber),
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

通过 react 源码中,可以看出 useState 是特殊的 useReducer

  • 可见useState不过就是个语法糖,本质其实就是useReducer
  • updateState 复用了 updateReducer(区别只是 updateState 将 reducer 设置为 updateReducer)
  • mountState 虽没直接调用 mountReducer,但是几乎大同小异(区别只是 mountState 将 reducer 设置为basicStateReducer)
更新:

分两种情况,是否是 reRender,所谓reRender就是说在当前更新周期中又产生了新的更新,就继续执行这些更新知道当前渲染周期中没有更新为止

他们基本的操作是一致的,就是根据 reducerupdate.action 来创建新的 state,并赋值给Hook.memoizedState 以及 Hook.baseState

注意这里,对于非reRender得情况,我们会对每个更新判断其优先级,如果不是当前整体更新优先级内得更新会跳过,第一个跳过得Update会变成新的baseUpdate他记录了在之后所有得Update,即便是优先级比他高得,因为在他被执行得时候,需要保证后续的更新要在他更新之后的基础上再次执行,因为结果可能会不一样。

来源

preact 中的

useState 源码解析

调用了 useReducer 源码

export function useState(initialState) {
	return useReducer(invokeOrReturn, initialState);
}

useReducer 源码解析

// 模块全局定义
/** @type {number} */
let currentIndex; // 状态的索引,也就是前面模拟实现 useState 时候所说的指针

let currentComponent; // 当前的组件

export function useReducer(reducer, initialState, init) {
	/** @type {import('./internal').ReducerHookState} */
    // 通过 getHookState 方法来获取 hooks 
	const hookState = getHookState(currentIndex++);

	// 如果没有组件 也就是初始渲染
	if (!hookState._component) {
		hookState._component = currentComponent;
		hookState._value = [
			// 没有 init 执行 invokeOrReturn
				// invokeOrReturn 方法判断 initialState 是否是函数
				// 是函数 initialState(null) 因为初始化没有值默认为null
				// 不是函数 直接返回 initialState
			!init ? invokeOrReturn(null, initialState) : init(initialState),

			action => {
				// reducer == invokeOrReturn
				const nextValue = reducer(hookState._value[0], action);
				// 如果当前的值,不等于 下一个值
				// 也就是更新的状态的值,不等于之前的状态的值
				if (hookState._value[0]!==nextValue) {
					// 储存最新的状态
					hookState._value[0] = nextValue;
					// 渲染组件
					hookState._component.setState({});
				}
			}
		];
	}
    // hookState._value 数据格式也就是 [satea:any, action:Function] 的数据格式拉
	return hookState._value;
}

getHookState 方法

function getHookState(index) {
	if (options._hook) options._hook(currentComponent);
	const hooks = currentComponent.__hooks || (currentComponent.__hooks = { _list: [], _pendingEffects: [], _pendingLayoutEffects: [] });

	if (index >= hooks._list.length) {
		hooks._list.push({});
	}
	return hooks._list[index];
}

invokeOrReturn 方法

function invokeOrReturn(arg, f) {
	return typeof f === 'function' ? f(arg) : f;
}

总结

使用 hooks 几个月了。基本上所有类组件都可以使用函数式组件来写。现在 react 社区的很多组件,都也开始支持hooks。大概了解了点重要的源码,做到知其然也知其所以然,那么在实际工作中使用他可以减少错误,提高效率。

最后

全文章,如有错误或不严谨的地方,请务必给予指正,谢谢!

参考:

Redux 的模拟讲解

写在前面

用最基础的方法讲解 Redux 实现原理?说白了其实是我能力有限,只能用最基础的方法来讲解,为了讲的更加清楚,文章可能比较拖沓。不过我相信,不是很了解 Redux 的同学,看完我今天分享的文章一定会有所收获!

什么是 Redux ?

这不是我今天要说的重点,想知道什么是 Redux 点击传送门

开始

在开始之前我想先讲一种常用的设计模式:观察者模式。先来说一下我对观察者模式的个人理解:观察者模式(Publish/Subscribe)模式。对于这种模式很清楚的同学下面这段代码可以跳过。如果你还不清楚,你可以试着手敲一遍下面的代码!!

观察者模式

观察者模式,基于一个主题/事件通道,希望接收通知的对象(称为subscriber)通过自定义事件订
阅主题,通过deliver发布主题事件的方式被通知。就和用户订阅微信公众号道理一样,只要发布,用户就能接收到最新的内容。

/**
 * describe: 实现一个观察者模式
 */
let data = {
    hero: '凤凰',
};
//用来储存 订阅者 的数组
let subscribers = [];
//订阅 添加订阅者 方法
const addSubscriber = function(fn) {
    subscribers.push(fn)
}
//发布
const deliver = function(name) {
    data.hero = name;
    //当数据发生改变,调用(通知)所有方法(订阅者)
    for(let i = 0; i<subscribers.length; i++){
        const fn = subscribers[i]
        fn()
    }
}
//通过 addSubscriber 发起订阅
addSubscriber(() => {
    console.log(data.hero)
})
//改变data,就会自动打印名称
deliver('发条') 
deliver('狐狸')
deliver('卡牌')

这个发布订阅通过 addSubscriber 来储存订阅者(方法fn),当通过调用 deliver 来改变数据的时候,就会自动遍历 addSubscriber 来执行里面的 fn 方法 。

为啥要讲这个发布订阅模式呢?因为搞清楚了这个模式那么接下来你读该文章就会感觉更加清晰。

Redux 起步

首先我们把上面那个发布订阅代码优化一下,顺便改一下命名,为什么要改命名?主要是紧跟 Redux 的步伐。让同学们更加眼熟。

let state = {hero: '凤凰'};
let subscribers = [];
//订阅 定义一个 subscribe 
const subscribe = (fn) => {
    subscribers.push(fn)
}
//发布
const dispatch = (name) => {
    state.hero = name;
    //当数据发生改变,调用(通知)所有方法(订阅者)
    subscribers.forEach(fn=>fn())
}
//通过 subscribe 发起订阅
subscribe(() => {
    console.log(state.hero)
})
//改变state状态,就会自动打印名称
//这里要注意的是,state状态不能直接去修改
dispatch('发条') 
dispatch('狐狸')
dispatch('卡牌')

现在这样一改是不是很眼熟了,没错这就是一个类似redux改变状态的思路。但是光一个发布订阅还是不够的,不可能改变一个状态需要去定义这么多方法。所以我们把他封装起来。

creatStore 方法

const creatStore = (initState) => {
    let state = initState;
    let subscribers = [];
    //订阅 定义一个 subscribe 
    const subscribe = (fn) => {
        subscribers.push(fn)
    }
    //发布
    const dispatch = (currentState) => {
        state = currentState;
        //当数据发生改变,调用(通知)所有方法(订阅者)
        subscribers.forEach(fn=>fn())
    }
    // 这里需要添加这个获取 state 的方法
    const getState = () => {
        return state;
    }
    return {
        subscribe,
        dispatch,
        getState,
    }
}

这样就创建好了一个 createStore 方法。没有什么新东西,就传进去一个初始状态,然后在返回 subscribe, dispatch, getState 三大方法。这里新增了个 getState 方法,代码很简单就是一个 return state 为了获取 state.

creatStore 使用

实现了 createStore 下面我们来试试如何使用他,那就拿那个非常经典的案例--计数器来试试

let initState = {
    num: 0,
}
const store = creatStore(initState);
//订阅
store.subscribe(() => {
    let state = store.getState()
    console.log(state.num)
})
// +1
store.dispatch({
   num: store.getState().num + 1
})
//-1
store.dispatch({
   num: store.getState().num - 1
})

这个样子又接近了一点 Redux 的模样。 不过这样有个问题。如果你使用 store.dispatch 方法时,中间万一写错了或者传了个其他东西那就比较麻烦了。就比如下面这样:

其实我是想 +1,+1,-1 最后应该是 1 (初始 num 为0)!但是由于写错了一个导致后面的都会错。而且他还有个问题就是可以随便的给一个新的状态。那么就显得不那么单纯了。比如下面这样:

因为恶意修改 num 为 String 类型,导致后面在使用 dispatch 由于 num 不再是 Number 类型,导致打印出 NaN,这就不是我们想要的啦。所以我们要在改造一下,让 dispatch 变得单纯一些。那要怎么做呢?我们请一个管理者来帮我们管理,暂且给他命名 reducer

为什么叫 reducer

我在 reducer 官网中找到下面这段介绍 reducer


什么意思,对于这种英语上来我就是有道翻译一下

当然这个翻译感觉并没什么作用,

找一找中文 Redux 官网,他是这样说的:

之所以将这样的函数称之为reducer,是因为这种函数与被传入 Array.prototype.reduce(reducer, ?initialValue) 里的回调函数属于相同的类型。保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作。

诶,这个翻译似乎就清楚了很多。正如下面评论者说的一样 灵感来自于数组中reduce方法,是一种运算合成。那么说到这里我就来介绍一下 reduce。

什么是 reduce

话不多说直接上代码

const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue;

// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));
// expected output: 10

// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5));
// expected output: 15

/*该减速作用有四个参数:
*累加器(acc)
*当前价值(cur)
*当前指数(idx)
*源数组(src)
*您的reducer函数的返回值被分配给累加器,其值在整个阵列的每次迭代中被记住,并最终成为最终的单个结果值。
*/

具体参数介绍

callback
  函数在数组中的每个元素上执行,有四个参数:
accumulator
  累加器累加回调的返回值; 它是先前在回调调用中返回的累计值,或者initialValue,如果提供(参见下文)。
currentValue
  当前元素在数组中处理。
currentIndex可选的
  数组中正在处理的当前元素的索引。如果initialValue提供了an,则从索引0开始,否则从索引1开始
array可选的
  该阵列reduce()被召唤。
initialValue可选的
  用作第一次调用的第一个参数的值callback。如果未提供初始值,则将使用数组中的第一个元素。调用reduce()没有初始值的空数组是一个错误。

这个方法相对比 forEach, map, filter 这个理解起来还是算比较困难的。也可以看 MDN 的 Array.prototype.reduce() 详细介绍

注:首先感谢下面评论者 panda080 的指导,受他的建议,我重新去 Rudex 官网寻找。通过学习自己也更加的理解了 reducer 和 reduce reducer官网

ps:理解完之后,其实个人觉得 reducer 这个命名从翻译过来的角度总觉得很怪异。可能英语有限,或许他有更加贴切的意思我还不知道。

什么是 reducer

reducer 在我学习的过程中我把他认为是个管理者(可能这个认为是不正确的),然后我们每次想做什么就去通知管理者,让他在来根据我们说的去做。如果我们不小心说错了,那么他就不会去做。直接按默认的事情来。噔噔蹬蹬 reducer 登场!!

function reducer(state, action) {
    //通过传进来的 action.type 让管理者去匹配要做的事情
    switch (action.type){
        case 'add':
            return {
                ...state,
                count: state.count + 1
            }
        case 'minus':
            return {
                ...state,
                count: state.count - 1
            }
        // 没有匹配到的方法 就返回默认的值
        default:
            return state;
    }
}

增加了这个管理者,那么我们就要重新来写一下之前的 createStroe 方法了:把 reducer 放进去

const creatStore = (reducer,initState) => {
    let state = initState;
    let subscribers = [];
    //订阅 定义一个 subscribe 
    const subscribe = (fn) => {
        subscribers.push(fn)
    }
    //发布
    const dispatch = (action) => {
        state = reducer(state,action);
        subscribers.forEach(fn=>fn())
    }
    const getState = () => {
        return state;
    }
    return {
        subscribe,
        dispatch,
        getState,
    }
}

很简单的一个修改,为了让你们方便看出修改的地方,和区别,我特意重新码了这两个前后的方法对比,如下图

好,接下来我们试试添加了管理者的 creatStore 效果如何。

function reducer(state, action) {
    //通过传进来的 action.type 让管理者去匹配要做的事情
    switch (action.type){
        case 'add':
            return {
                ...state,
                num: state.num + 1
            }
        case 'minus':
            return {
                ...state,
                num: state.num - 1
            }
        // 没有匹配到的方法 就返回默认的值
        default:
            return state;
    }
}

let initState = {
    num: 0,
}
const store = creatStore(reducer,initState);
//订阅
store.subscribe(() => {
    let state = store.getState()
    console.log(state.num)
})

为了看清楚结果,dispatch(订阅)我直接在控制台输出,如下图:

效果很好,我们不会再因为写错,而出现 NaN 或者其他不可描述的问题。现在这个 dispatch 比较纯粹了一点。

我们只是给他一个 type ,然后让管理者自己去帮我们处理如何更改状态。如果不小心写错,或者随便给个 type 那么管理者匹配不到那么这个动作那么我们这次 dispatch 就是无效的,会返回我们自己的默认 state。

好叻,现在这个样子基本上就是我脑海中第一次使用 redux 看到的样子。那个时候我使用起来都非常困难。当时勉强实现了一下这个计数器 demo 我就默默的关闭了 vs code。

接下来我们再完善一下这个 reducer,给他再添加一个方法。并且这次我们再给 state 一个

function reducer(state, action) {
    //通过传进来的 action.type 让管理者去匹配要做的事情
    switch (action.type){
        case 'add':
            return {
                ...state,
                num: state.num + 1
            }
        case 'minus':
            return {
                ...state,
                num: state.num - 1
            }
        // 增加一个可以传参的方法,让他更加灵活
        case 'changeNum':
            return {
                ...state,
                num: state.num + action.val
            }
        // 没有匹配到的方法 就返回默认的值
        default:
            return state;
    }
}

let initState = {
    num: 0,
}
const store = creatStore(reducer,initState);
//订阅
store.subscribe(() => {
    let state = store.getState()
    console.log(state.num)
})

控制台再使用一次新的方法:

好叻,这样是不是就让 dispatch 更加灵活了。

现在我们在 reducer 中就写了 3 个方法,但是实际项目中,方法一定是很多的,那么都这样写下去,一定是不利于开发和维护的。那么这个问题就留给大家去思考一下。

提示:Redux 也知道这一点,所以他提供了 combineReducers 去实现这个模式。这是一个高阶 Reducer 的示例,他接收一个拆分后 reducer 函数组成的对象,返回一个新的 Reducer 函数。

思考完之后可以参考 redux 中文文档 的combineReducers介绍

总结

Redux 这个项目里,有很多非常巧妙的方法,很多地方可以借鉴。毕竟这可是在 github 上有 47W+ 的 Star。

今天也只是讲了他的一小部分。自己也在努力学习中,希望今后能分享更多的看法,并和大家深入探讨。

写在最后

上述每个案例,和代码我都托管在 github 上了,分享给大家可以直接打开即用。github 传送门

全文章,如有错误或不严谨的地方,请务必给予指正,谢谢!

参考:

Fetch 的实例讲解

简介

Fetch API 提供了一个 JavaScript接口,用于访问和操纵 HTTP 管道的部分,例如请求和响应。它还提供了一个全局 fetch()方法,该方法提供了一种简单,合乎逻辑的方式来跨网络异步获取资源。

fetch() 必须接受一个参数---路径。无论请求成功与否,它都返回一个 Promise 对象,resolve 对应请求的 Response。另外你还可以设置第二个参数(可选的参数)options(常用配置如下,具体详情参见 Request)。

配置

options={
    // 请求方式 GET,POST,等
    method: "GET", 
    
    // 请求头headers   
    headers: {
        Accept: 'application/json, text/plain, */*',
        'Content-Type': 'application/json;charset=UTF-8', 
        'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
	},
	
    // omit: 从不发送cookies.
    // same-origin 只有当URL与响应脚本同源才发送 cookies
    // include 总是发送请求资源域在本地的 cookies 验证信息
    credentials: 'include'
    
    //包含请求的模式 (例如: cors, no-cors, same-origin, navigate)
    //no-cors: 常用于跨域请求不带CORS响应头场景
    //cors表示同域和带有CORS响应头的跨域下可请求成功. 其他请求将被拒绝
    mode: 'cors'
    
    //包含请求的缓存模式 (例如: default, reload, no-cache).具体参数见下面(cache参数)
    cache: 'default'
}
cache参数

cache 表示如何处理缓存, 遵守 http 规范, 拥有如下几种值:

  1. default: 浏览器从 HTTP 缓存中寻找匹配的请求.
  2. no-store: 浏览器在不先查看缓存的情况下从远程服务器获取资源,并且不会使用下载的资源更新缓存
  3. reload: 请求之前将忽略 http 缓存的存在, 但请求拿到响应后, 它将主动更新 http 缓存.
  4. no-cache: 如果存在缓存, 那么 fetch 将发送一个条件查询 request 和一个正常的 request , 拿到响应后, 它会更新http缓存.
  5. force-cache: 浏览器在其HTTP缓存中查找匹配的请求。
    • 如果存在匹配,新鲜或过时,则将从缓存中返回。
    • 如果没有匹配,浏览器将发出正常请求,并使用下载的资源更新缓存
  6. only-if-cached: fetch 强行取缓存,( 即使缓存过期了也从缓存取). 如果没有缓存, 浏览器将返回错误

案例

这是一个比较基本的案例,这里为了看的清楚,没有处理 catch 问题 。

var url = 'https://zhidao.baidu.com/question/api/hotword?pn=1561&rn=5&t=1551165472017';
fetch(url).then(function(response) {
  return response;
}).then(function(data) {
  console.log(data);
})

可以自己动手把这个代码直接贴到控制台中 , 这里为了防止同源策略,并且看到更详细的数据我们最好在 https://zhidao.baidu.com 内的控制台中输入。如果在其他的页面我们应该在 fetch 方法的第二个参数中加上{ mode: "no-cors" }。为了看的更加清楚,我贴出这两种情况的打印结果,如下所示:

https://zhidao.baidu.com 网页控制台输入的地址

在其他网页控制台输入的地址

我们会发现两个打印结果基本不同,首先跨域打印的结果中很多数据都是空的,另外我们也发现其中 type 也不相同,这里介绍下 response.type。

fetch请求的响应类型( response.type )为如下三种之一:

  • basic,同域下, 响应类型为 “basic”。
  • cors,跨域下,返回 cors 响应头, 响应类型为 “cors”。
  • opaque,跨域下, 服务器没有返回 cors 响应头, 响应类型为 “opaque”。

fetch设了{mode='no-cors'}表示不垮域,可以请求成功,但拿不到服务器返回数据

看到这里,心急的同学会发现,其实现在数据还是没有拿到,我们仅仅只是拿到了个 Response 对象。那么如何拿到数据呢?

瞧好:

var url = 'https://zhidao.baidu.com/question/api/hotword?pn=1561&rn=5&t=1551165472017';
fetch(url).then(function(response) {
  //通过 response 原型上的 json 方法拿到数据,在返回出去
  return response.json();
}).then(function(data) {
  // 接收到 data 打印出来
  console.log(data);
})

通过 response 原型上的 json 方法拿到数据,在返回出去。response 原型打印如下:

这里要注意的是 response .json / response.text 方法只能使用一个并且只能使用一次,同时使用两个,或则使用两次都会报如下错误:

Uncaught (in promise) TypeError: Failed to execute 'json' on 'Response': body stream is locked

为什么不能使用两次?

数据流只能读取一次,一旦读取,数据流变空,再次读取会报错。可以使用 response.clone() 复制一个副本。

为什么只能读取一次?

答案还在查寻中,同时也希望知道的读者能够指点一下。

上面的写法,看起来有回调,又有链式调用,我们试着换个新写法,这里使用 async/await 来实现:

var url = 'https://zhidao.baidu.com/question/api/hotword?pn=1&rn=5&t=1551191647420';
let getData = async () => {
    let response = await fetch(url);
    let data = response.json();
    console.log(data)
}

打印 data 如下:

这里 data 是一个 Promise 对象,由于他的内部变量[[PromiseValue]]在外部无法得到,只能在 then 中获取,所以在后面加上 then 获取 data

let response = await fetch(url);
let data = response.json();
data.then((res)=>{
  console.log(res)
})
// 搞定 res 就是我们要的数据拉

封装

我们通过 fetch 来做个简易的 request 封装,顺便把 options 再回顾一遍,很多配置在实际中可能是不需要的,都有默认配置(options 的默认配置在下面括号中)。

function request(url, data = {}) {
  // options配置的默认选项在括号中
    return fetch(url, {
        method: "POST", //(GET), POST, PUT, DELETE, etc
        mode: "cors", // (same-origin), no-cors, cors
        cache: "no-cache", // (default), no-cache, reload, force-cache, only-if-cached
        credentials: "same-origin", //(same-origin), include, omit
        headers: {
            "Content-Type": "application/json",
            // "Content-Type": "application/x-www-form-urlencoded",
        },
        redirect: "follow", //(follow), manual, error
        referrer: "no-referrer", //(client), no-referrer
        body: JSON.stringify(data), // 这里格式要与 "Content-Type" 同步
    })
    .then(response => response.json());
}

最后

初次写帖,如有错误或不严谨的地方,请务必给予指正,谢谢

参考:

git 小总结

											Git版本管理以及、github和码云仓库的应用

1.如何查看git操作?

答:.git文件中会记录每次用户的每次操作,可以通过cat .git/config查看配置

2.Git基本配置

(1)配置用户基本信息:

$git config --global user.name zouxiaomingya
$git config --global user.eamil [email protected]

(2)配置行尾及颜色:

$git config --global core.autocrlf true
$git config --global core.autocrlf input
$git config --global color.ui auto
$git diff

说明:红色为正在进行处理、绿色通常指运行正常

(3)Git config 优先级

优先级local>global>system

3.init --项目初始化

(1)建立一个目录并初始化仓库

$mkdir git-tutorial
$cd git -tutorial
$git init

说明:在github上新建仓库的时候,可以指定是私有还是公开,可以创建一个readme文件来对项目做一些简单说明,在.gitignore通常用来说明哪些文件不希望纳入版本控制之中

(2)如何将本地仓库与github仓库(远程)连接

$git remote add origin xxxxx.git
$git push -u origin master

4.commit --源码提交

(1)command(命令行方式)

$ls -la 可查看当前目录下的文件状态,如果有修改会有颜色区分,有点类似于git status,需要生成、查看或编辑文件时touch、cat/vi xx.index.html;
$git add xx.index.html 将文件添加到暂存区的持久化容器中
$git status 可以查看输出状态已经不同了,由红色变成绿色,每一次提交更改都需经过暂存区,这是Git架构的关键部分
$git commit -m "modify introduce" 如果直接git commit,git实际上会打开一个文本编辑器,让你输入,输入完后,执行esc + :wq,否则将不会允许提交

(2)github官网

点击进入对应的文件,点击edit,在浏览器中会提供一个简单的文本编辑器,编辑完成后输入commit summary也可以进行commit change。

(3)桌面版github

可以选择暂存哪些文件然后提交,支持批处理,比在github官网上要有效率多了,可以作为比较完美的对命令行的方式做个替代

5.diff --类似于SVN diff 查看文件更改

常规使用:

$git diff

特殊场景(使用技巧):

(1)假设已经存档了某些文件(即add 进了暂存区),想查看暂存区文件和最近提交的历史文件有什么区别?

$git diff --staged

(2) 如果想继续修改暂存区的文件(暂存并不意味着不能再次修改它),如果继续对同一文件进行修改,在git status的时候会提示文件已暂存,未缓存修改,diff命令可以帮助跳过正在暂存的区域,并告知在最后一次commit之后所做的所有修改。输入以下命令,将工作树和头一次提交相比较。

$git diff HEAD

(3) 上述命令可能只能得到一个比较笼统的修改范围(某行或某块小区域),不能体现到具体的某个小改动(某个文字或数字),这是Git 处理文本差异的方式,有时并不十分有用,可以加上

$git diff --color-word或者$git diff --word-diff

(4)阻止diff显示修改的内容,仅仅显示更改了的文件,针对这类场景(可能只是需要锁定一个被修改过的特定文件)

$git diff -stat

5.log --日志输出,了解仓库的管理

简要说明:在我们的终端直接键入$git log, 会显示项目的commit信息,最上面的记录是我们最新的提交,最早的提交位于最底部,每条日志信息由四部分构成。如果有多次提交,可以用上下方向键来滚动提交内容。

1.40个字符的十六进制编码--提交的唯一标志,提交引用;

2.提交者的用户名和邮件地址;

3.提交发生的时间;

4.提交的内容本身。

(1).过滤日志信息,仅显示commitid简写和提交的内容,很清晰的了解整个项目的脉络和演变。

$git log --oneline

(2).查看每次提交包含了哪些文件,相当于在$git log 的基础上多出了一项,即文件的变更(代码的增删改查)信息,相对改变也会以加减符号列出。

$git log --stat

(3).查看每次提交哪些内容改变了,相当于在$git log --stat的基础上多出了一项,具体哪些内容改变了,有点类似于diff操作,只不过针对的对象不同,这里针对的是已经commit过的。

$git log --patch

(4).以上功能可以进行排列组合来得到想要的输出日志,具体指代含义也是一目了然,不予赘述。

$git log --patch --oneline 也可以 $git log --stat --oneline

(5).展示提交结构为$git log --graph,也可以增加几个选项(排列组合),来简洁输出并得到一些其他的信息。这里的话会给我们展示每次提交的一行概括,包含提供每个分支的标签以及提交的其他的标志。

$git log --graph --all --decorate --online

6.remove --删除文件

简要说明:此段只要是列举出删除文件的两种方式,一种是add,还有一种是remove。

(1)如果只有一个文件要删除,直接$git rm file.txt,会从文件系统中删除该文件,并且会暂存这个文件已经被删除的事实,可以通过git status查看。如果提交了,这个文件不会从之前的历史中消失,但会从未来的提交中消失。有时候未留意直接$rm file.txt,发现文件已经消失,但是通过git status 查看时发现改变没有被暂存,这时依然可以通过$git rm命令来暂存它(并不在意文件已经不存在了)。

$git rm file.txt(后面两行目的一致,意义基本相同)
$rm file.txt
$git rm file.txt

(2)在实际的清除场景中,可能有许多文件要删除,不可能每次通过命令行来操作。git status 时发现已经有些文件已经被删除了,通过其他途径,这时通过$git add -u .会遍历工作树,寻找之前已经识别的文件,现在要消失的文件,他会它们作为新的删除来暂存。后面这个.是当前工作目录的简写。当它看到add时会从当前坐在目录低轨道最深处去寻找能够添加的文件,或者说在这个场景下能够删除的所有文件。

$git add -u .

(3)有时可能需要删除一个文件,但并不想从文件系统中真正的删除它。换个说法,想告诉git,不再跟踪这个文件,但是把它保留在工作树中,具体命令如下所示。如果不想被不在跟踪的这个文件影响,可以用ignore来处理,后续补充说明。

$git rm --cached file.txt

以上操作也可通过github官方桌面端实现,很方便。

7.move --移动文件

简要说明:主要是介绍几种移动文件的不同方式,通过使用mv命令以及add命令。

(1)mv命令处理。通过git status可以看到git已经暂存了move发生的事实。

$git mv 原路径 目标路径 ==>  例:git mv head.jpg source/head.jpg

如果只是简单的使用mv命令移动文件,忘了告诉git。git注意到某个地方出现了一个本不应该出现的新文件,同时注意到原来的文件区域已经被删除掉了,这时可以通过以下命令开解决它。

$mv prod.log production.log
$git rm  prod.log //删除原来的文件
$git add production.log //添加新文件

(2)add命令处理。在实际移动过程中个,可能会通过finder以及ide等其他渠道来移动文件,类似于批处理,这个时候用以下命令即可处理,.代表从当前目录无限向下递归。

$git add -A . //发现所有移动过去的新文件,删除所有原来的旧文件

上述移动文件简单情况下可以这样处理。在实际应用过程中,从一个目录移动到另一个目录时可能会改变一点文件,这种是不受影响的。

$git add -A tutorials/
$git status //可得知git仍然把它看做一次移动
$git commit -m "moved lesson4 into the proper subfolder" //每一次最终都要commit

(3)用日志命令来展示文件在移动过程中的历史。

$git log --stat --tutorials/intermediate/lesson6.html(当前/最新的路径)  =>跟SVN选中文件显示日志一个效果,不会定位到移动的日志。
$git log --stat -M --follow --tutorials/intermediate/lesson6.html (告诉日志在文件移动过程中跟踪文件,just can reach the result)

在提交移动时git会提供一个相似度阈值,默认为50%,超过这个值git会在移动过程中追踪它。认为它是一个移动而不仅仅是删除和添加。但是这个阈值可以我们自己配置,通过在开关-M之后加一个数字。

8.ignore --忽视、避免暂存或提交文件

(1)The .gitignore File。在工程中创建一个.gitignore文件,并上传到仓库,ignore匹配规则应用到目录中的所有文件。前面加个#号可以添加注释,鼓励这种做法。具体处理如下,也可以直接在github官网上直接进行编辑。

$touch .gitignore
$git add .gitignore
$git commit -m "Preparing to ignore temporary files"
$vim .gitignore 
insert: 
		#sass缓存不纳入版本管理
		.sass-cache
		*.log
		tmp/
:wq

(2)选择忽略模式。如果想查看仓库中哪些文件被忽略了,又不想打开.gitignore看规则。

$git ls-file --others --ignored --exclude-standard //显示出被忽略的文件

9.branch --创建、删除/转换分支

简要说明:master主干是对产品特性的一种体现,当我们需要多业务并行开发就可以新建一个分支,然后在分支上自由工作,保证我们安全工作并远离master分支。

(1)创建和删除分支(Creating&Deleting)

$git branch => 查看分支
$git branch app-startup-scripts(新分支名字)
$git branch -d 分支名字 =>如果他没有完全被合并将会报错,告诉你将-d替换成-D,忽略未合并的事实,强制删除,后续章节会讲解合并

(2)转换/切换分支(Switching)

值得一提的是我们切换分支时任何出现在面板区的工作或者工作目录都会跟着我们转过来,如果我们处于这个活动目录或者面板区的文件被覆盖的话是无法切换到新分支的。什么意思了?打个比方在当前分支下有个index.html,你add进暂存区了,但是没有提交,然后自己又在这个文件目录下,就会出现这种情况。还有一种情况你 $ls 的时候可能看到文件消失和重现,在切换分支的时候不用担心这个,他们会待在各自的分支里,直到被合并为止。

$git checkout 分支名字

10.checkout --改变分支

(1)查看当前在那个分支可以通过git branch(带*号的)或者git status

(2)除了上述提到的用来切换分支之外,另一个目的就是把你项目下的工作树、目录和文件等东西做一个更加详细的commit。执行完下面这条命令后,会处于detached HEAD(游离状态),在这个状态下进行的commit不会对你的远程分支产生影响。要深入理解游离状态以及应用可参考https://blog.csdn.net/trochiluses/article/details/8991701,讲的很透彻。

$git checkout 提交引用(commit Id)

(3)checkout 另一个作用是用来撤销和丢弃编辑的内容。

$git checkout index.text (丢弃当前修改,保持跟暂存区/上一次提交的一致)

(4)缩短一些其他步骤,如果想创建一个分支然后切换到它,只需要运行:

$git checkout -b 新分支的名字

11.merge --分支合并
简要说明:把分支和多条线的历史操作汇聚起来。
1.git merge
通过merge可以把两个j或者多个分支的历史汇聚起来然后在工作树中展示,所有这些分支全部commit的累积结果。对于一个经典的工作流,我们通过运行如下指令来合并我们需要的commit。

    $git merge 分支名字

2.解决冲突
合并过程中遇到冲突意味着两个文件的变化非常的相似,Git不知道如何去解决这些冲突,或者把他们合并在一个文件里。
为了解决这个我们可以直接git status查看冲突文件,然后手动解决冲突在git add-git commit一遍

3.--abort 指令
有一种情况是遇到了文件冲突,但是并不想马上解决它或者只想抛弃它,这个时候可以运行以下指令,这将会从你当前分支的最后一次提交中清除你的工作目录,并且同样会清除暂存区

$git merge --abort

4.--squash指令
如果不想把历史汇聚起来,但是想要一个具体分支中的全部commit简单的运行。

$git merge --squash 目标分支上的名字

在你目前切换到的分支上创建一个新的提交,这代表了发生在目标分支上的所有改变

5.清除已经合并完的分支
一旦成功合并完了分支,没必要保存分支标签或者名字,因为可以从最终的分支或者当前的分支看到所有的历史,所有的提交,要做的就是

$git branch -d 分支名字

12.Network
1.远端(Remotes)

$git remote add 目的地名字 整个URL地址
$git remote add set-url 目的地名字 修改URL地址
$git remote rm 远端名字
$git remote -v 得到全部的输出

远端追踪的分支是分支间的中间人,这些分支有一些不同的唯一原因是在所有分支名字前面有一个前缀用来响应远程控制的,大部分情况加这个origin/。

2.fetch、pull、push
fetch命令本身是去GitHub.com抓取任何信息下载下来,把它放在远程追踪分支里。

$git fetch origin
$git pull origin
$git push origin

13.图形化用户界面
推荐sourceTree , github for mac , windows GUI, 免费的远程仓库,码云以及github for personal

14.Reset
简要说明:git reset 是一个允许你塑造仓库历史的命令,无论你想撤销一些修改还是把你的commit弄得不一样,都可以用这个来实现。这里主要简述三种模式,分别是soft、mixed、hard模式

mixed模式,它修改了历史和工作目录,所以叫做混合模式,修改了多个东西。
soft模式,选取一条或多条commit,把他们的全部改变放回到暂存区,让你在此基础上继续创建新的提交
hard模式,是一个具有破坏性的操作,这意味着要删除你不在想保存的东西

1.mixed是推荐模式,大部分新手都是用它,因为它们会显示在status命令中d。当你在暂存区有一些改动的时候,输入git status,我们可以看到会出现git reset head ,这允许我们把这些改变移除暂存区,把它们放回在工作目录中,使用mixed选项。

2.soft是这样一种方式,我把一些改动,这些改动可能太过分散,但这些commit都属于同一个事项把它们全部弄到一起通过以下命令,所以通过以下命令可以将这5条commit当做一次提交,压缩进暂存区

$git reset --soft HEAD~5 将最新的5条提交

3.如果你想完全丢掉一些你的工作,可能想丢掉一些不再发挥作用的改动,可以通过以下指令来完全丢掉这些提交

$git reset --hard

扩展:有另外一个相像的命令经常l拿来与reset讨论--checkout,两者其实有些不同,它经常操作的是整个仓库的历史,checkout更加关注在一个目录或者文件级别的精度上,不是撤销或者改变某一条提交,我们可以回到某一个特定文件的某次提交历史上,然后把这个文件和版本拉回到我们当前的工作目录。

15.Reflog
1、 Git reflog
它会追踪你对修改内容的修改。

Git用户首先会发现他们在仓库的每次提交是一个实时的记录快照,会展示代码库是怎么发展的,但是更高级的git用户会发现reflog会追踪制造的commit以及丢弃的commit。这提供了一个30天的缓冲时间,在这期间,你可以从任何错误中回复,包括git reset命令带来的不好的部分,分支的删除或者可能rebase消失了。

Git reset对每个分支都很特别,你会注意到,如果你选择去观察,在.git目录下有一个子目录叫做logs,在那下面,每个分支都有具体的文件,你本地仓库里有的分支。每个这些日志都追踪你做的变动,包括前进和后退方向的。Git reflog命令是做到这个的用户接口,它打印出大部分最近的历史,分页机制让你可以通过已经发生的旧的入口去到你目前已经切换到的分支上。

2、 Using a GUI
Reflog提供的线性历史是很难查看的,哪一个是孤单的分支?哪一个是不在代码库里一部分的提交,一些有技巧和创造性的命令行可以把reflog的结果通过管道到一个图形用户界面:

$ gitk --all ‘ git reflog | cut -c1-7 ’ &

这样很容易看出来哪些分支是独立的,哪些提交不再是这个分支的一部分,哪些东西你想要恢复,通过捕获他们的哈希值和使用reset --hard命令。

3、 Frequent commits

Git reflog是你做频繁提交的动力,频繁提交意味着,在reflog里有历史存储着,不管你reset了它们,抛弃了提交,修改它们还是犯了一个错误。没有提交的所有东西,在工作区或者暂存区都是有风险的。但是,如果它被提交了,你可以恢复它们,这最近的30天有一个保证措施允许你采取大的,勇敢的步骤使用Git的其他特性。

javaScript 基础

模拟 New 操作符
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例

创建一个空的简单 JavaScript 对象(即 {});
连接该对象(即设置该对象的构造函数)到另一个对象 ;
将步骤1新创建的对象作为this的上下文 ;
如果该函数没有返回对象,则返回this。
用法实例讲解
当构造函数没有返回值时:
function Animal (name, age) {
this.name = name;
this.age = age;
this.speak = '汪汪'
}
Animal.prototype.color = 'red';

Animal.prototype.say = function () {
console.log('我会叫--' + this.speak);
}

var dog = new Animal('旺财', '3');

console.log(dog.name) // 旺财
// 打印一个没有的属性
console.log(dog.eat) // undefined
console.log(dog.age) // 3
console.log(dog.speak) // 汪汪
console.log(dog.color) // red
dog.say(); // 我会叫--汪汪
当构造函数有返回值,且返回值类型为对象的(不包含 null )
有返回对象的,new 实例化后的对象就是返回的对象。拿不到内部其他的属性,也不会获取到原型上的属性和方法

function Animal2 (name, age) {
this.name = name;
this.age = age;
this.speak = '汪汪'
return {
name,
eat: '吃骨头',
}
}
Animal2.prototype.color = 'red';

Animal2.prototype.say = function () {
console.log('我会叫--' + this.speak);
}

var dog = new Animal2('旺财', '3');

console.log(dog.name) // 旺财
console.log(dog.eat) // 吃骨头
console.log(dog.age) // undefined
console.log(dog.speak) // undefined
console.log(dog.color) // undefined
dog.say(); // 报错: dog.say is not a function
构造函数返回 null,
这种情况实际上和第一种情况一样(相当于构造函数没有返回值)

当时确实要注意的是,typeof null === 'object' 所有在实现的时候,我们要进行处理。不能看到 typeof 返回的是object,那么就返回该值。具体实例看下面的讲解。

function Animal3 (name, age) {
this.name = name;
this.age = age;
this.speak = '汪汪'
return null
}
Animal3.prototype.color = 'red';

Animal3.prototype.say = function () {
console.log('我会叫--' + this.speak);
}

var dog = new Animal3('旺财', '3');

console.log(dog.name) // 旺财
console.log(dog.age) // 3
console.log(dog.speak) // 汪汪
console.log(dog.color) // red
dog.say(); // 我会叫--汪汪
实现代码
function fakeNew() {

var obj = {};
// 获取第一个参数,并且会删除第一个参数
var Constructor = [].shift.call(arguments);

// obj 继承构造函数上的方法
obj.__proto__ = Constructor.prototype;

// Constructor 方法中的 this 指向 obj, 执行 Constructor 方法,相当于给 obj 继承了Constructor 中的属性,方法。 (可以理解就是通过 apply 实现继承)
var result = Constructor.apply(obj, arguments);
if(typeof result === 'object'){
    // result || obj 防止返回的是 null(因为 typeof null == 'object');
    return result || obj;
} else {
    return obj;
}

};
我们来简写一下 New ,通过 ES6 写很简单 3 行代码搞定。

function fakeNew(Constructor, ...rest) {
// Object.create 方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
var obj = Object.create(Constructor.prototype),
var result = Constructor.apply(obj, rest);
return typeof result === 'object' ? result || obj : obj;

};
为什么会通过 Object.create 方法来实现让对象的原型继承 Constructor.prototype 属性呢?

由于现代 JavaScript 引擎优化属性访问所带来的特性的关系,更改对象的 [[Prototype]]在各个浏览器和 JavaScript 引擎上都是一个很慢的操作。其在更改继承的性能上的影响是微妙而又广泛的,这不仅仅限于 obj.proto = ... 语句上的时间花费,而且可能会延伸到任何代码,那些可以访问任何[[Prototype]]已被更改的对象的代码。如果你关心性能,你应该避免设置一个对象的 [[Prototype]]。相反,你应该使用 Object.create()来创建带有你想要的[[Prototype]]的新对象。

出自 MDN 原话

也就是说 Object.setPrototypeOf(obj, Constructor.prototype) 和

var obj = {};obj.proto = Constructor.prototype 两种方法,无论在写法还是性能上都没有 Object.create 方法来的更好

apply做了什么?
上面模拟实现的 new 方法中 var result = Constructor.apply(obj, rest); 这行代码做了什么?

其实就是通过 apply 来实现属性和方法的继承。不清楚的同学可以看下 apply 的模拟实现过程。

apply 的模拟实现
Function.prototype.fakeApply = function (obj, arr) {
var context = obj || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn();
} else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')')
}
delete context.fn
return result;
}
我们用 ES6 写法来简写一下 apply

Function.prototype.fakeApply = function(obj, arr = []) {
var context = obj || window;
context.fn = this;
result = context.fn(...arr);
delete context.fn;
return result
};
同理 call 的模拟方法

// 通过...arr 接收后面所有的参数
Function.prototype.fakeCall = function(obj, ...arr) {
var context = obj || window;
context.fn = this;
// 将接受的参数展开执行
result = context.fn(...arr);
delete context.fn;
return result
};
在开发过程中根据实际情况选择使用方式,那么都可以用的话建议选择 call 方法,因为 call 的性能高于 aplly。

因为 apply 第二个参数是数组,最终执行的时候还是要将数组转变成一个一个参数,来执行函数,因此性能比 apply 差 ( 也就是差一点点 )

高阶函数
Array.prototype.map
Array.prototype.myMap = function(fn){
const arr = this;
const newArr = [];
for(let i = 0; i<arr.length; i++){
var temp = fn(arr[i], i, arr)
newArr.push(temp);
}
return newArr;
}
完整的 map 他是有第二个参数的

Array.map( callback [, thisArg ] )

callback

原数组中的元素调用该方法后返回一个新数组。它接收三个参数,分别为 currentValue、index、array。

currentValue

​ callback的第一个参数,数组中当前被传递的元素。

index

​ callback的第二个参数,数组中当前被传递的元素的索引。

array

​ callback的第三个参数,调用map()方法的数组,即原数组。

thisArg

执行callback函数时this指向的对象

Array.prototype.map polyfill源码实现:地址传送门

// 实现 ECMA-262, Edition 5, 15.4.4.18
// 参考: http://es5.github.io/#x15.4.4.18
if (!Array.prototype.map) {
Array.prototype.map = function(callback, thisArg) {

var T, A, k;

if (this == null) {
  throw new TypeError(" this is null or not defined");
}

// 1. 将O赋值为调用map方法的数组.
var O = Object(this);

// 2.将len赋值为数组O的长度.
var len = O.length >>> 0;

// 3.如果callback不是函数,则抛出TypeError异常.
if (Object.prototype.toString.call(callback) != "[object Function]") {
  throw new TypeError(callback + " is not a function");
}

// 4. 如果参数thisArg有值,则将T赋值为thisArg;否则T为undefined.
if (thisArg) {
  T = thisArg;
}

// 5. 创建新数组A,长度为原数组O长度len
A = new Array(len);

// 6. 将k赋值为0
k = 0;

// 7. 当 k < len 时,执行循环.
while(k < len) {

  var kValue, mappedValue;

  //遍历O,k为原数组索引
  if (k in O) {

    //kValue为索引k对应的值.
    kValue = O[ k ];

    // 执行callback,this指向T,参数有三个.分别是kValue:值,k:索引,O:原数组.
    mappedValue = callback.call(T, kValue, k, O);

    // 返回值添加到新数组A中.
    A[ k ] = mappedValue;
  }
  // k自增1
  k++;
}

// 8. 返回新数组A
return A;

};
}
Array.prototype.reduce
简易实现

Array.prototype.myReduce = function(callback, initialValue ) {
var previous = initialValue, k = 0, length = this.length;
// 如果没有传入 initialValue 就默认将数组的第 0 个赋给 previous
if (typeof initialValue === "undefined") {
previous = this[0];
k = 1;
}
if (typeof callback === "function") {
for (k; k < length; k++) {
this.hasOwnProperty(k) && (previous = callback(previous, this[k], k, this));
}
}
return previous;
};
Array.prototype.forEach
简易实现

Array.prototype.MyForEach = function(fn) {
const arr = this
for(let i = 0; i<arr.length; i++){
fn(arr[i],i,arr)
}
}
深拷贝
const deepCopy = function(obj){
return Object.keys(obj).reduce(function (copy, item){
// 如果对象的 value 类型为 object 就再次执行 deepCopy 来实现深拷贝
copy[item] = typeof obj[item] === 'object' ? deepCopy(obj[item]) : obj[item];
return copy;
// 判断 obj 是 数组类型 还是对象类型 这样 copy 就是 [] 或者是 {}
}, Object.prototype.toString.call(obj) === '[object Array]' ? [] : {})
}
使用箭头函数和 ,逗号运算符,简化

const deepCopy = obj => Object.keys(obj).reduce(
(copy, item) => (copy[item] = typeof obj[item] === 'object' ? deepCopy(obj[item]) : obj[item], copy)
,Object.prototype.toString.call(obj) === '[object Array]' ? [] : {}
);
javaScript 循环
for 循环
for(var i=1;i<=3;i++)
{
  for(var i=1;i<3;i++)
  {
    console.log(i)
  }
}
// 1 2

for(let i=1;i<=3;i++)
{
  for(let i=1;i<3;i++)
  {
    console.log(i)
  }
}
// 1 2 1 2 1 2
do-while
先执行一次再去做判断

var a = 10
do{
console.log(a--)
} while (a>2)
while
先判断, 再执行

while(i<=10)
{
  循环体
  i++
}
for in
实例
var obj = {
a: 1,
b: [],
c: function () {}
};
for (var key in obj) {
console.log(key);
}
// 结果是:
// a
// b
// c
注意:
Object.prototype.objCustom = function() {};
Array.prototype.arrCustom = function() {};

var arr = [3, 5, 7];
arr.foo = 'hello';

for (var i in arr) {
console.log(i);
}
// 结果是:
// 0
// 1
// 2
// foo
// arrCustom
// objCustom
自定义的 Array.prototype 上的方法也会遍历出来,通过原型链最后还能遍历到 arr.proto.proto Object.prototype 上的objCustom

解决方案:
hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性

Object.prototype.objCustom = function() {};
Array.prototype.arrCustom = function() {};

var arr = [3, 5, 7];
arr.foo = 'hello';

for (var i in arr) {
if (arr.hasOwnProperty(i)) {
console.log(i);
}
}
// 结果是:
// 0
// 1
// 2
// foo
数组上面本身的属性还是会遍历出来

如果不想要本身的属性可以通过 forEach 来实现

Object.prototype.objCustom = function() {};
Array.prototype.arrCustom = function() {};

var arr = [3, 5, 7];
arr.foo = 'hello';

arr.forEach((item, index)=>{
console.log(item, index)
})
// 结果是:
// 3 0
// 5 1
// 7 2
或者通过 for of 来实现

for of
for of 需要遍历可迭代(iterable)的类型

var obj = {
a: 1,
b: [],
c: function () {}
};
for (var key of obj) {
console.log(key);
}
// 出错:
// Uncaught TypeError: obj is not iterable
ES6 标准不认为 Object 对象是可迭代的,所以不能用 for-of 遍历之

可迭代的类型有 Array、 Map、Set、arguments 类对象、NodeList 这类 DOM 集合、String**、**generators

最后
全文章,如有错误或不严谨的地方,请务必给予指正,谢谢!

参考

MDN文档
鑫空间,鑫生活

Axios 源码解读

Axios 源码解读

Axios 是什么?

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

Axios 功能

  • 从浏览器中创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

希望通过源码来慢慢理清这些功能的实现原理

Axios 使用

执行 GET 请求

axios.get('/user?ID=12345')
  .then(function (response) {
    console.log(response);
  })

执行 POST 请求

axios.post('/user', {
    name: 'zxm',
    age: 18,
  })
  .then(function (response) {
    console.log(response);
  })

使用方式不是本次主题的重点,具体使用方式可以参照 Axios 中文说明

源码拉下来直接进入 lib 文件夹开始解读源码

源码解读

lib/ axios.js 开始

'use strict';

var utils = require('./utils');
var bind = require('./helpers/bind');
var Axios = require('./core/Axios');
var mergeConfig = require('./core/mergeConfig');
var defaults = require('./defaults');

// 重点 createInstance 方法
// 先眼熟一个代码 下面讲完工具函数会再具体来讲解 createInstance
function createInstance(defaultConfig) {
    // 实例化 Axios
  var context = new Axios(defaultConfig);
    // 自定义 bind 方法 返回一个函数()=> {Axios.prototype.request.apply(context,args)}
  var instance = bind(Axios.prototype.request, context);
    // Axios 源码的工具类 
  utils.extend(instance, Axios.prototype, context);
    
  utils.extend(instance, context);
    
  return instance;
}
// 传入一个默认配置   defaults 配置先不管,后面会有具体的细节
var axios = createInstance(defaults);


// 下面都是为 axios 实例化的对象增加不同的方法。
axios.Axios = Axios;
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
module.exports = axios;
module.exports.default = axios;

lib/ util.js 工具方法

有如下方法:

module.exports = {
  isArray: isArray,
  isArrayBuffer: isArrayBuffer,
  isBuffer: isBuffer,
  isFormData: isFormData,
  isArrayBufferView: isArrayBufferView,
  isString: isString,
  isNumber: isNumber,
  isObject: isObject,
  isUndefined: isUndefined,
  isDate: isDate,
  isFile: isFile,
  isBlob: isBlob,
  isFunction: isFunction,
  isStream: isStream,
  isURLSearchParams: isURLSearchParams,
  isStandardBrowserEnv: isStandardBrowserEnv,
  forEach: forEach,
  merge: merge,
  deepMerge: deepMerge,
  extend: extend,
  trim: trim
};

is开头的isXxx方法名 都是判断是否是 Xxx 类型 ,这里就不做明说 主要是看下 后面几个方法

extend 将 b 里面的属性和方法继承给 a , 并且将 b 里面的方法的执行上个下文都绑定到 thisArg

// a, b,thisArg 参数都为一个对象
function extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {
      // 如果指定了 thisArg 那么绑定执行上下文到 thisArg
    if (thisArg && typeof val === 'function') {
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  });
  return a;
}

抽象的话看个例子

这样是不是就一目了然。fn2 函数没有拿自己对象内的 age = 20 而是被指定到了 thisArg 中的 age

自定义 forEach 方法遍历基本数据,数组,对象。

function forEach(obj, fn) {
  if (obj === null || typeof obj === 'undefined') {
    return;
  }
  if (typeof obj !== 'object') {
    obj = [obj];
  }
  if (isArray(obj)) {
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

merge 合并对象的属性,相同属性后面的替换前的

function merge(/* obj1, obj2, obj3, ... */) {
  var result = {};
  function assignValue(val, key) {
    if (typeof result[key] === 'object' && typeof val === 'object') {
      result[key] = merge(result[key], val);
    } else {
      result[key] = val;
    }
  }

  for (var i = 0, l = arguments.length; i < l; i++) {
    forEach(arguments[i], assignValue);
  }
  return result;
}

如下图所示:

bind -> lib/ helpers/ bind.js 这个很清楚,返回一个函数,并且传入的方法执行上下文绑定到 thisArg上。

module.exports = function bind(fn, thisArg) {
  return function wrap() {
    var args = new Array(arguments.length);
    for (var i = 0; i < args.length; i++) {
      args[i] = arguments[i];
    }
    return fn.apply(thisArg, args);
  };
};

好勒那么 axios/util 方法我们就基本没有问题拉

看完这些工具类方法后我们在回过头看之前的 createInstance 方法

function createInstance(defaultConfig) {
    // 实例化 Axios, Axios下面会讲到
  var context = new Axios(defaultConfig);
    
    // 将 Axios.prototype.request 的执行上下文绑定到 context
    // bind 方法返回的是一个函数
  var instance = bind(Axios.prototype.request, context);
    
    // 将 Axios.prototype 上的所有方法的执行上下文绑定到 context , 并且继承给 instance
  utils.extend(instance, Axios.prototype, context);
    
    // 将 context 继承给 instance
  utils.extend(instance, context);
    
  return instance;
}
// 传入一个默认配置  
var axios = createInstance(defaults);

总结:createInstance 函数返回了一个函数 instance.

  1. instance 是一个函数 Axios.prototype.request 且执行上下文绑定到 context。
  2. instance 里面还有 Axios.prototype 上面的所有方法,并且这些方法的执行上下文也绑定到 context。
  3. instance 里面还有 context 上的方法。

Axios 实例源码

'use strict';
var utils = require('./../utils');
var buildURL = require('../helpers/buildURL');
var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');
var mergeConfig = require('./mergeConfig');

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

// 核心方法 request 
Axios.prototype.request = function request(config) {
  // ... 单独讲
};

// 合并配置将用户的配置 和默认的配置合并
Axios.prototype.getUri = function getUri(config) {
  config = mergeConfig(this.defaults, config);
  return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, '');
};
// 这个就是给 Axios.prototype 上面增加 delete,get,head,options 方法
// 这样我们就可以使用 axios.get(), axios.post() 等等方法
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
     // 都是调用了 this.request 方法
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

module.exports = Axios;

上面的所有的方法都是通过调用了 this.request 方法

那么我们就来看这个 request 方法,个人认为是源码内的精华也是比较难理解的部分,使用到了 Promise 的链式调用,也使用到了中间件的**。

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
Axios.prototype.request = function request(config) {
  // Allow for axios('example/url'[, config]) a la fetch API
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }
    // 合并配置
  config = mergeConfig(this.defaults, config);
    // 请求方式,没有默认为 get
  config.method = config.method ? config.method.toLowerCase() : 'get';
    
    // 重点 这个就是拦截器的中间件
  var chain = [dispatchRequest, undefined];
    // 生成一个 promise 对象
  var promise = Promise.resolve(config);

    // 将请求前方法置入 chain 数组的前面 一次置入两个 成功的,失败的
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
	// 将请求后的方法置入 chain 数组的后面 一次置入两个 成功的,失败的
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

   // 通过 shift 方法把第一个元素从其中删除,并返回第一个元素。
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

看到这里有点抽象,没关系。我们先讲下拦截器。在请求或响应被 then 或 catch 处理前拦截它们。使用方法参考 Axios 中文说明 ,大致使用如下。

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  });

通过 promise 链式调用一个一个函数,这个函数就是 chain 数组里面的方法

// 初始的 chain 数组 dispatchRequest 是发送请求的方法
var chain = [dispatchRequest, undefined];

// 然后 遍历 interceptors 
// 注意 这里的 forEach 不是 Array.forEach, 也不是上面讲到的 util.forEach. 具体 拦截器源码 会讲到
// 现在我们只要理解他是遍历给 chain 里面追加两个方法就可以
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

然后添加了请求拦截器和相应拦截器 chain 会是什么样子呢 (重点)

chain = [ 请求拦截器的成功方法,请求拦截器的失败方法,dispatchRequest, undefined, 响应拦截器的成功方法,响应拦截器的失败方法 ]。

好了,知道具体使用使用之后是什么样子呢?回过头去看 request 方法

每次请求的时候我们有一个

 while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
   意思就是将 chainn 内的方法两两拿出来执行 成如下这样
    promise.then(请求拦截器的成功方法, 请求拦截器的失败方法)
           .then(dispatchRequest, undefined)
           .then(响应拦截器的成功方法, 响应拦截器的失败方法)

现在看是不是清楚了很多,拦截器的原理。现在我们再来看 InterceptorManager 的源码

InterceptorManager 拦截器源码

lib/ core/ InterceptorManager.js

'use strict';
var utils = require('./../utils');

function InterceptorManager() {
    // 存放方法的数组
  this.handlers = [];
}
// 通过 use 方法来添加拦截方法
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};
// 通过 eject 方法来删除拦截方法
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};
// 添加一个 forEach 方法,这就是上述说的 forEach
InterceptorManager.prototype.forEach = function forEach(fn) {
    // 里面还是依旧使用了 utils 的 forEach, 不要纠结这些 forEach 的具体代码
    // 明白他们干了什么就可以
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

module.exports = InterceptorManager;

dispatchRequest 源码

lib/ core/ dispatchRequest .js

'use strict';
var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
var isAbsoluteURL = require('./../helpers/isAbsoluteURL');
var combineURLs = require('./../helpers/combineURLs');
// 请求取消时候的方法,暂不看
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);
    // 请求没有取消 执行下面的请求
  if (config.baseURL && !isAbsoluteURL(config.url)) {
    config.url = combineURLs(config.baseURL, config.url);
  }
  config.headers = config.headers || {};
	// 转换数据
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );
    // 合并配置
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );
  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );
    // 这里是重点, 获取请求的方式,下面会将到
  var adapter = config.adapter || defaults.adapter;
  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);
	// 难道了请求的数据, 转换 data
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );
    return response;
  }, function onAdapterRejection(reason) {
      // 失败处理
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);

      // Transform response data
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });
};

看了这么多,我们还没看到是通过什么来发送请求的,现在我们看看在最开始实例化 createInstance 方法中我们传入的 defaults 是什么

var axios = createInstance(defaults);

lib/ defaults.js

'use strict';

var utils = require('./utils');
var normalizeHeaderName = require('./helpers/normalizeHeaderName');

var DEFAULT_CONTENT_TYPE = {
  'Content-Type': 'application/x-www-form-urlencoded'
};

function setContentTypeIfUnset(headers, value) {
  if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {
    headers['Content-Type'] = value;
  }
}
// getDefaultAdapter 方法是来获取请求的方式
function getDefaultAdapter() {
  var adapter;
  // process 是 node 环境的全局变量
  if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // 如果是 node 环境那么久通过 node http 的请求方法
    adapter = require('./adapters/http');
  } else if (typeof XMLHttpRequest !== 'undefined') {
   // 如果是浏览器啥的 有 XMLHttpRequest 的就用 XMLHttpRequest
    adapter = require('./adapters/xhr');
  }
  return adapter;
}

var defaults = {
    // adapter 就是请求的方法
  adapter: getDefaultAdapter(),
	// 下面一些请求头,转换数据,请求,详情的数据
    // 这也就是为什么我们可以直接拿到请求的数据时一个对象,如果用 ajax 我们拿到的都是 jSON 格式的字符串
    // 然后每次都通过 JSON.stringify(data)来处理结果。
  transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Accept');
    normalizeHeaderName(headers, 'Content-Type');
    if (utils.isFormData(data) ||
      utils.isArrayBuffer(data) ||
      utils.isBuffer(data) ||
      utils.isStream(data) ||
      utils.isFile(data) ||
      utils.isBlob(data)
    ) {
      return data;
    }
    if (utils.isArrayBufferView(data)) {
      return data.buffer;
    }
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      return data.toString();
    }
    if (utils.isObject(data)) {
      setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
      return JSON.stringify(data);
    }
    return data;
  }],

  transformResponse: [function transformResponse(data) {
    /*eslint no-param-reassign:0*/
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
  }],

  /**
   * A timeout in milliseconds to abort a request. If set to 0 (default) a
   * timeout is not created.
   */
  timeout: 0,

  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',

  maxContentLength: -1,

  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  }
};

defaults.headers = {
  common: {
    'Accept': 'application/json, text/plain, */*'
  }
};

utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
  defaults.headers[method] = {};
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});

module.exports = defaults;

总结

  1. Axios 的源码走读一遍确实可以看到和学习到很多的东西。
  2. Axios 还有一些功能:请求的取消,请求超时的处理。这里我没有全部说明。
  3. Axios 通过在请求中添加 toke 并验证方法,让客户端支持防御 XSRF Django CSRF 原理分析

最后

如果看的还不是很明白,不用担心,这基本上是我表达,书写的不够好。因为在写篇文章时我也曾反复的删除,重写,总觉得表达的不够清楚。为了加强理解和学习大家可以去 github 将代码拉下来对照着来看。

git clone https://github.com/axios/axios.git

全文章,如有错误或不严谨的地方,请务必给予指正,谢谢!

React_hooks

什么是 useState ?

首先 useState 是一个Hook,它允许您将React状态添加到功能组件

useState 是一个方法,它本身是无法存储状态的

其次,他运行在 FunctionalComponent 里面,本身也是无法保存状态的

useState 只接收一个参数 inital value,并看不出有什么特殊的地方。

为什么要 useState?

因为类组件有很多的痛点

  1. 很难复用逻辑(只能用HOC,或者render props),会导致组件树层级很深
  2. 会产生巨大的组件(指很多代码必须写在类里面)
  3. 类组件很难理解,比如方法需要bindthis指向不明确
    比如 经常看到这样的写法。
// 可能是这样
class MyComponent extends React.Component {
  constructor() {
    // initiallize
    this.handler1 = this.handler1.bind(this)
    this.handler2 = this.handler2.bind(this)
    this.handler3 = this.handler3.bind(this)
    this.handler4 = this.handler4.bind(this)
    this.handler5 = this.handler5.bind(this)
    // ...more
  }
}

// 可能是这样的
export default withStyle(style)(connect(/*something*/)(withRouter(MyComponent)))

怎么用 useState?

开始之前先看一个简单的例子,在没有 Hooks 之前我们是这样来写的。

import React, {Component} from 'react';
class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      Switch: "打开"
    };
  }
  setSwitch = () => {
    this.state.Switch === "打开"
      ? this.setState({ Switch: "关闭" })
      : this.setState({ Switch: "打开" });
  };
  render() {
    return (
      <div>
        <p>现在是: {this.state.Switch}状态</p>
        <button onClick={this.setSwitch}>Change Me!</button>
      </div>
    );
  }
}
export default CommonCollectionPage;

以前函数式组件需要给他自己的状态的时候我们总是不得不把函数式组件变成 Class 类组件,现在有了 React Hooks 我们在也不需要因为一个小状态而将函数式组件变成类组件,上面的这个例子,就可以变成下面的这个方法来表现。

useState 使用案例

function App() {
  const [Switch, setSwitch] = useState("打开");
  const newName = () =>
    Switch === "打开" ? setSwitch("关闭") : setSwitch("打开");
  return (
    <div>
      <p>现在是: {Switch} 状态</p>
      <button onClick={newName}>Change Me!</button>
    </div>
  );
}

所以 useState 就是为了给函数式组件添加一个可以维护自身状态的功能。

操作地址传送

useState 注意事项

动态传递参数给 useState 的时候只有第一次才会生效

useEffect 是什么?

通过 useEffect 方法来代替了 class 类组件的 componentDidMount, componentDidUpdate, componentWillUnmount

三个组件,那如何一个钩子怎么使用才有 3 个钩子的不同效果呢:

首选我们先运行起来:

useEffect 第一个参数

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  const [switch, setSwitch] = useState("打开");
  const handleSwitch = _ =>
    switch === "打开" ? setSwitch("关闭") : setSwitch("打开");
  const [num, setNum] = useState(0);
  const add = () => {
    setNum(num + 1);
  };
  const minus = () => {
    setNum(num - 1);
  };
  useEffect(() => {
    console.log("改变了状态");
  }, [num]);
  return (
    <div>
      <p>现在是: {switch}状态</p>
      <button onClick={handleSwitch}>Change Me!</button>
      <p>数字: {num}</p>
      <button onClick={add}> +1 </button>
      <button onClick={minus}> -1 </button>
    </div>
  );
}

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

上面这个例子他会再初次渲染的时候打印 改变了状态 并且每次状态改变的时候都打印 改变了状态

那么这样的用法就是 componentDidMount, componentDidUpdate,的使用

useEffect 第二个参数

useEffect 方法中加入第二个参数 一个空对象 [ ] 那么之后只会在首次组件装载好的时候打印一次

改变状态了

现在我们需要当 num 改变状态的时候 去打印 怎么办呢?

刚刚我们使用了一个空对象 那么只需要再这个对象中加入我们需要监听的状态那么他相当于使用了 , componentDidUpdate, 钩子函数例如

useEffect(() => {
    console.log("改变了状态");
  }, [num]);

那么现在,当 activeUser 状态改变的时候我们会发现又打印出了 改变状态 这句话。而当 switch 状态改变的时候并不会打印这句话。

useEffect 的闭包使用

import React, { useState, useEffect } from 'react';
function App() {
  useEffect(() => {
    console.log('装载了')
    return () => {
      console.log('卸载拉');
    };
  });
  return (
    <div>
      xxxx
    </div>
  );
}
export default App;

在 useEffect 中我们可以做两件事情,组件挂载完成时候,还有组件卸载时,只要在 useEffect 中使用闭包,在闭包中做我们想要在组件卸载时需要做的事就可以。

this.state 和 useState

首先我们回顾下以前我们常常用的类组件,下面是一段实现计数器的代码:

import React, { Component, useState, useEffect } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

class App extends Component {
  constructor() {
    super();
    this.state = {
      count: 0
    };
  }
  componentDidMount() {
    setTimeout(() => {
      console.log(`count:${this.state.count}`);
    }, 3000);
  }
  componentDidUpdate() {
    setTimeout(() => {
      console.log(`count:${this.state.count}`);
    }, 3000);
  }
  render() {
    return (
      <div>
        <p>{this.state.count}</p>
        <button
          onClick={() =>
            this.setState({
              count: this.state.count + 1
            })
          }
        >
          点击 3 次
        </button>
      </div>
    );
  }
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

页面刷新立即,点击 3 次按钮,上述这个例子的打印结果会是什么????

我们很清楚的了解 this.state 和 this.setState 所有我们会知道打印的是:

一段时间后依此打印 3,3,3,3

不过 hooks 中 useEffect 的运行机制并不是这样运作的。

function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setTimeout(() => {
      console.log(count);
    }, 3000);
  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        点击 3 次
      </button>
    </div>
  );
}

一段时间后依此打印 0,1,2,3。

其实没有以前 this.state 使用印象,看到这段代码的打印结果,会认为这不是理所当然的吗?

那我们想让上面的类组件,也实现上述 0,1,2,3,4 效果 我们增加这 2 行代码

//...

componentDidMount() {
    // 新增此行代码
    const newCount = this.state.count; 
    setTimeout(() => {
      console.log(newCount);
    }, 3000);
  }
  componentDidUpdate() {
    // 新增此行代码
    const newCount = this.state.count; 
    setTimeout(() => {
      console.log(newCount);
    }, 3000);
  }

  // ....

所有我们会联想到 在函数式组件中 取值下面的效果是一样的

function Counter(props) {
  useEffect(() => {
    setTimeout(() => {
      console.log(props.counter);
    }, 3000);
  });
  // ...
}
function Counter(props) {
  const counter = props.counter;
  useEffect(() => {
    setTimeout(() => {
      console.log(counter);
    }, 3000);
  });
  // ...
}

这一点说明了在渲染函数式组件的时候他的更新不会改变渲染范围内的 props ,state 的值。(表达可能有误)

当然,有时候我们会想在effect的回调函数里读取最新的值而不是之前的值。就像之前的类组件那样打印 3.3.3.3。这里最简单的实现方法是使用useRef。

useRef 的使用方式

首先实现上诉打印 3,3,3,3 的问题。如下代码所示

function Counter() {
  const [count, setCount] = useState(0);
  const latestCount = useRef(count);
  useEffect(() => {
    latestCount.current = count;
    setTimeout(() => {
      console.log(latestCount.current);
    }, 3000);
  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        点击 3 次
      </button>
    </div>
  );
}

我们通过 useRef(initVal) 来返回一个可变的 ref 对象,其 current 属性被初始化为传递的参数 (initVal)。

useRef 也可以获取 DOM 节点

然后函数式组件没有生命周期,那我们怎么才能获取 ChartDom 真实的 dom 元素呢?也可以通过 useRef 实现

import React, { useState, useRef, Fragment, useEffect } from 'react';
import { Button } from 'antd';

function Demo({ count: propsCount = 1 }) {
  const [count, setCount] = useState(propsCount);
  const refContainer = useRef(null); // 如同之前的 React.createRef();
  
  useEffect(() => {
    console.log(refContainer.current, '>>>>>>>>>>>');
  });
  
  return (
      <Fragment>
        <Button onClick={() => { setCount(count + 1); }}>Click Me</Button>
        <p ref={refContainer}>You click {count} times</p>
      </Fragment>
  );
}

export default Demo;

useReducer 使用

import React, { useReducer, useEffect } from "react";
import ReactDOM from "react-dom";

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { count, step } = state;

  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    </>
  );
}

const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  if (action.type === 'tick') {
    return { count: count + step, step };
  } else if (action.type === 'step') {
    return { count, step: action.step };
  } else {
    throw new Error();
  }
}

useImperativeHandle 使用

在类组件中我们都是通过使用 ref 的方式 获取类组件的实例,这样就可以让父组件调用子组件的方法。

那么函数式没有实例,怎么使用 ref 呢?

// 子组件
import React, { useRef, useImperativeHandle,forwardRef } from "react";
function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
export default forwardRef(FancyInput);
// 父组件
import React, { useRef } from "react";
function App(){
  const fancyInputRef = useRef(null)
  // 这样获取子组件方法
  fancyInputRef.current.focus()
  return (
    <div> 
      <FancyInput ref={fancyInputRef} /> 
    </div>
  )

}

最后

实战使用 hooks 也快 3 4 个月了。确实感觉到了 React 慢慢变得更加强大,函数式组件使用也是非常精简和方便,个人认为脱离了 Class 代码的可读性,可维护性也感觉更加高了。

全文章,如有错误或不严谨的地方,请务必给予指正,谢谢!

个人其他文章推荐:

  1. Axios 源码解读
  2. Fetch 实例讲解

参考:

flex 布局

弹性盒布局 / 弹性盒模型 / flex 布局 ...

弹性盒的作用:

解决元素的排列问题(移动端或者流式布局)排排坐吃果果。

弹性盒的兼容问题:

  1. 弹性盒子在IE中有一小部分的兼容不同,在其他浏览器也是如此,需要加上前缀

  2. 弹性盒有两个版本display:box; display:flex; 有的浏览器只支持box版本,开发新版本之后利用autoprefixer工具来处理兼容。

注意,使用了弹性盒子之后,盒模型就变成了弹性盒模型了,就和标准盒模型有很多区别了,又成一个体系。

弹性盒语法学习

参考文献

下图是弹性盒模型的图片

弹性盒模型

因为我们要解决的是子元素在父元素中的排列问题,所以弹性盒使用的第一步就是为父元素添加 display:flex; 属性,添加之后,父元素就变成弹性父元素,里面的子元素变成了弹性子元素(跟后代无关), 而且弹性子元素还可以添加 display:flex变成弹性父元素 ...

弹性盒模型中我们要明白几个新概念,弹性盒模型形成之后,伴随着会生成两个轴(主轴和侧轴),元素排列都是按照轴排列,通过控制轴来进行子元素排列

弹性子元素会按照主轴排列,主轴默认的方向是水平方向

还有四个属性:main-start main-end (描述主轴的开始方法和结束方向)cross-start cross-end (描述侧轴的开始方法和结束方向),控制元素排列的时候可以让元素根据某几个点来排列

弹性父元素有这样的几个属性:

  1. flex-direction 调整主轴的方向

    row (主轴水平 默认)、row-reverse (水平,从右边开始)、column (主轴垂直)、column-reverse(垂直 ,从下边开始)

  2. flex-wrap 当子元素的主轴上的尺寸加起来已经超过父元素主轴上的尺寸,控制是否需要换行,不换行需要压缩子元素的尺寸

    no-wrap (不换行 默认) 、wrap (换行)、wrap-reverse (换行并且反转侧轴)

  3. flex-flow 是direction和wrap的复合属性 column

  4. justify-content 控制子元素在主轴上的排列方式

    flex-start (子元素在主轴开始位置排列 默认) 、
    flex-end (子元素在主轴结束位置排列)、
    center (子元素在主轴中间位置排列)、
    space-between (左右两头各一个,中间的隔开排)、
    space-around

  5. align-items 控制子元素在侧轴上的排列方式
    flex-start、flex-end、center、
    baseline ( 子元素在侧轴上以文字基线为标准排列 )、
    stretch ( 子元素没有高度的时候,会扩展子元素的高度到容器。。 )

  6. align-content 控制换行之后,多个行之间的排列方式
    stretch( 子元素没有高度的时候,会扩展子元素的高度到容器。。 )、
    flex-start、
    flex-end、
    center、
    space-between
    space-around

弹性子元素有这样的几个属性:

  1. order 给子元素排序

    子元素默认值为0,当子元素的order值相同的时候,排列按照html的顺序排, order值小的排前面

  2. flex-grow 控制子元素占用剩余空间

    默认值为0,

    子元素的占用 = 全部剩余空间 / 所有子元素的flex-grow的值 * 当前子元素的flex-grow

  3. flex-shrink 决定某个子元素相对于其他普通子元素的收缩大小。

    默认值为 1,如果设置为0,则不收缩。

    子元素的收缩 = 全部超出空间 / 所有子元素的flex-shrink的值 * 当前子元素的flex-shrink

  4. flex 是flex-grow 和 flex-shrink的符合属性

    flex: 1 1;

  5. align-self 控制单个的子元素脱离其他的子元素自己在侧轴方向上的排列方式

    值就是align-items的值

Hooks 源码模拟与解读

useState 解析

useState 使用

通常我们这样来使用 useState 方法

function App() {
  const [num, setNum] = useState(0);
  const add = () => {
    setNum(num + 1);
  };
  return (
    <div>
      <p>数字: {num}</p>
      <button onClick={add}> +1 </button>
    </div>
  );
}

useState 的使用过程,我们先模拟一个大概的函数

function useState(initialValue) {
  var value = initialValue
  function setState(newVal) {	
    value = newVal
  }
  return [value, setState]
}

这个代码有一个问题,在执行 useState 的时候每次都会 var _val = initialValue,初始化数据;

于是我们可以用闭包的形式来保存状态。

const MyReact = (function() {
   // 定义一个 value 保存在该模块的全局中
  let value
  return {
    useState(initialValue) {
      value = value || initialValue 
      function setState(newVal) {
        value = newVal
      }
      return [value, setState]
    }
  }
})()

这样在每次执行的时候,就能够通过闭包的形式 来保存 value。

不过这个还是不符合 react 中的 useState。因为在实际操作中会出现多次调用,如下。

function App() {
  const [name, setName] = useState('Kevin');
  const [age, setAge] = useState(0);
  const handleName = () => {
    setNum('Dom');
  };
  const handleAge = () => {
    setAge(age + 1);
  };
  return (
    <div>
      <p>姓名: {name}</p>
      <button onClick={handleName}> 改名字 </button>
 	  <p>年龄: {age}</p>
      <button onClick={handleAge}> 加一岁 </button>
    </div>
  );
}

因此我们需要在改变 useState 储存状态的方式

useState 模拟实现

const MyReact = (function() {
  // 开辟一个储存 hooks 的空间
  let hooks = []; 
  // 指针从 0 开始
  let currentHook = 0 
  return {
    // 伪代码 解释重新渲染的时候 会初始化 currentHook
    render(Component) {
      const Comp = Component()
      Comp.render()
      currentHook = 0 // 重新渲染时候改变 hooks 指针
      return Comp
    },      
    useState(initialValue) {
      hooks[currentHook] = hooks[currentHook] || initialValue
      const setStateHookIndex = currentHook
      // 这里我们暂且默认 setState 方式第一个参数不传 函数,直接传状态
      const setState = newState => (hooks[setStateHookIndex] = newState)
      return [hooks[currentHook++], setState]
    }
  }
})()

因此当重新渲染 App 的时候,再次执行 useState 的时候传入的参数 kevin , 0 也就不会去使用,而是直接拿之前 hooks 存储好的值。

hooks 规则

官网 hoos 规则中明确的提出 hooks 不要再循环,条件或嵌套函数中使用。

为什么不可以?

我们来看下

下面这样一段代码。执行 useState 重新渲染,和初始化渲染 顺序不一样就会出现如下问题

如果了解了上面 useState 模拟写法的存储方式,那么这个问题的原因就迎刃而解了。


useEffect 解析

useEffect 使用

初始化会 打印一次 ‘useEffect_execute’, 改变年龄重新render,会再打印, 改变名字重新 render, 不会打印。因为依赖数组里面就监听了 age 的值

import React, { useState, useEffect } from 'react';

function App() {
  const [name, setName] = useState('Kevin');
  const [age, setAge] = useState(0);
  const handleName = () => {
    setName('Don');
  };
  const handleAge = () => {
    setAge(age + 1);
  };
  useEffect(()=>{
    console.log('useEffect_execute')
  }, [age])
  return (
    <div>
      <p>姓名: {name}</p>
      <button onClick={handleName}> 改名字 </button>
      <p>年龄: {age}</p>
      <button onClick={handleAge}> 加一岁 </button>
    </div>
  );
}
export default App;

useEffect 的模拟实现

const MyReact = (function() {
  // 开辟一个储存 hooks 的空间
  let hooks = []; 
  // 指针从 0 开始
  let currentHook = 0 
  // 定义个模块全局的 useEffect 依赖
  let deps;
  return {
    // 伪代码 解释重新渲染的时候 会初始化 currentHook
    render(Component) {
      const Comp = Component()
      Comp.render()
      currentHook = 0 // 重新渲染时候改变 hooks 指针
      return Comp
    },      
    useState(initialValue) {
      hooks[currentHook] = hooks[currentHook] || initialValue
      const setStateHookIndex = currentHook
      // 这里我们暂且默认 setState 方式第一个参数不传 函数,直接传状态
      const setState = newState => (hooks[setStateHookIndex] = newState)
      return [hooks[currentHook++], setState]
    }
    useEffect(callback, depArray) {
      const hasNoDeps = !depArray
      // 如果没有依赖,说明是第一次渲染,或者是没有传入依赖参数,那么就 为 true
      // 有依赖 使用 every 遍历依赖的状态是否变化, 变化就会 true
      const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true
      // 如果有 依赖, 并且依赖改变
      if (hasNoDeps || hasChangedDeps) {
        // 执行 
        callback()
        // 更新依赖
        deps = depArray
      }
    },
        
  }
})()

useEffect 注意事项

依赖项要真实

依赖需要想清楚。

刚开始使用 useEffect 的时候,我只有想重新触发 useEffect 的时候才会去设置依赖

那么也就会出现如下的问题。

希望的效果是界面中一秒增加一岁

import React, { useState, useEffect } from 'react';

function App() {
  const [name, setName] = useState('Kevin');
  const [age, setAge] = useState(0);
  const handleName = () => {
    setName('Don');
  };
  const handleAge = () => {
    setAge(age + 1);
  };
  useEffect(() => {
    setInterval(() => {
      setAge(age + 1);
      console.log(age)
    }, 1000);
  }, []);
  return (
    <div>
      <p>姓名: {name}</p>
      <button onClick={handleName}> 改名字 </button>
      <p>年龄: {age}</p>
      <button onClick={handleAge}> 加一岁 </button>
    </div>
  );
}
export default App;

其实你会发现 这里界面就增加了 一次 年龄。究其原因:

**在第一次渲染中,age0。因此,setAge(age+ 1)在第一次渲染中等价于setAge(0 + 1)。然而我设置了0依赖为空数组,那么之后的 useEffect 不会再重新运行,它后面每一秒都会调用setAge(0 + 1) **

也就是当我们需要 依赖 age 的时候我们 就必须再 依赖数组中去记录他的依赖。这样useEffect 才会正常的给我们去运行。

所以我们想要每秒都递增的话有两种方法

方法一:

真真切切的把你所依赖的状态填写到 数组中

  // 通过监听 age 的变化。来重新执行 useEffect 内的函数
  // 因此这里也就需要记录定时器,当卸载的时候我们去清空定时器,防止多个定时器重新触发
  useEffect(() => {
    const id = setInterval(() => {
      setAge(age + 1);
    }, 1000);
    return () => {
      clearInterval(id)
    };
  }, [age]);

方法二

useState 的参数传入 一个方法。

注:上面我们模拟的 useState 并没有做这个处理 后面我会讲解源码中去解析。

useEffect(() => {
    setInterval(() => {
      setAge(age => age + 1);
    }, 1000);
  }, []);

useEffect 只运行了一次,通过 useState 传入函数的方式它不再需要知道当前的age值。因为 React render 的时候它会帮我们处理

这正是setAge(age => age + 1)做的事情。再重新渲染的时候他会帮我们执行这个方法,并且传入最新的状态。

所以我们做到了去时刻改变状态,但是依赖中却不用写这个依赖,因为我们将原本的使用到的依赖移除了。(这句话表达感觉不到位)

接口无限请求问题

刚开始使用 useEffect 的我,在接口请求的时候常常会这样去写代码。

props 里面有 页码,通过切换页码,希望监听页码的变化来重新去请求数据

// 以下是伪代码 
// 这里用 dva 发送请求来模拟

import React, { useState, useEffect } from 'react';
import { connect } from 'dva';

function App(props) {
  const { goods, dispatch, page } = props;
  useEffect(() => {
    // 页面完成去发情请求
   dispatch({
      type: '/goods/list',
      payload: {page, pageSize:10},
    });
    // xxxx 
  }, [props]);
  return (
    <div>
      <p>商品: {goods}</p>
	 <button>点击切下一页</button>
    </div>
  );
}
export default connect(({ goods }) => ({
  goods,
}))(App);

然后得意洋洋的刷新界面,发现 Network 中疯狂循环的请求接口,导致页面的卡死。

究其原因是因为在依赖中,我们通过接口改变了状态 props 的更新, 导致重新渲染组件,导致会重新执行 useEffect 里面的方法,方法执行完成之后 props 的更新, 导致重新渲染组件,依赖项目是对象,引用类型发现不相等,又去执行 useEffect 里面的方法,又重新渲染,然后又对比,又不相等, 又执行。因此产生了无限循环。

Hooks 源码解析

该源码位置: react/packages/react-reconciler/src/ReactFiberHooks.js

const Dispatcher={
  useReducer: mountReducer,
  useState: mountState,
  // xxx 省略其他的方法
}

mountState 源码

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
    /*
    mountWorkInProgressHook 方法 返回初始化对象
    {
        memoizedState: null,
        baseState: null, 
        queue: null,
        baseUpdate: null,
        next: null,
  	}
    */
  const hook = mountWorkInProgressHook();
 // 如果传入的是函数 直接执行,所以第一次这个参数是 undefined
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });

	/*
	定义 dispatch 相当于
	const dispatch = queue.dispatch =
	dispatchAction.bind(null,currentlyRenderingFiber,queue);
	*/ 
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    // Flow doesn't know this is non-null, but we do.
    ((currentlyRenderingFiber: any): Fiber),
    queue,
  ): any));

 // 可以看到这个dispatch就是dispatchAction绑定了对应的 currentlyRenderingFiber 和 queue。最后return:
  return [hook.memoizedState, dispatch];
}

dispatchAction 源码

function dispatchAction<A>(fiber: Fiber, queue: UpdateQueue<A>, action: A) {
  //... 省略验证的代码
  const alternate = fiber.alternate;
    /*
    这其实就是判断这个更新是否是在渲染过程中产生的,currentlyRenderingFiber只有在FunctionalComponent更新的过程中才会被设置,在离开更新的时候设置为null,所以只要存在并更产生更新的Fiber相等,说明这个更新是在当前渲染中产生的,则这是一次reRender。
所有更新过程中产生的更新记录在renderPhaseUpdates这个Map上,以每个Hook的queue为key。
对于不是更新过程中产生的更新,则直接在queue上执行操作就行了,注意在最后会发起一次scheduleWork的调度。
    */
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    didScheduleRenderPhaseUpdate = true;
    const update: Update<A> = {
      expirationTime: renderExpirationTime,
      action,
      next: null,
    };
    if (renderPhaseUpdates === null) {
      renderPhaseUpdates = new Map();
    }
    const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
    if (firstRenderPhaseUpdate === undefined) {
      renderPhaseUpdates.set(queue, update);
    } else {
      // Append the update to the end of the list.
      let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
      while (lastRenderPhaseUpdate.next !== null) {
        lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
      }
      lastRenderPhaseUpdate.next = update;
    }
  } else {
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);
    const update: Update<A> = {
      expirationTime,
      action,
      next: null,
    };
    flushPassiveEffects();
    // Append the update to the end of the list.
    const last = queue.last;
    if (last === null) {
      // This is the first update. Create a circular list.
      update.next = update;
    } else {
      const first = last.next;
      if (first !== null) {
        // Still circular.
        update.next = first;
      }
      last.next = update;
    }
    queue.last = update;
    scheduleWork(fiber, expirationTime);
  }
}

mountReducer 源码

多勒第三个参数,是函数执行,默认初始状态 undefined

其他的和 上面的 mountState 大同小异

function mountReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = mountWorkInProgressHook();
  let initialState;
  if (init !== undefined) {
    initialState = init(initialArg);
  } else {
    initialState = ((initialArg: any): S);
  }
	// 其他和 useState 一样
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
    null,
    // Flow doesn't know this is non-null, but we do.
    ((currentlyRenderingFiber: any): Fiber),
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

通过 react 源码中,可以看出 useState 是特殊的 useReducer

  • 可见useState不过就是个语法糖,本质其实就是useReducer
  • updateState 复用了 updateReducer(区别只是 updateState 将 reducer 设置为 updateReducer)
  • mountState 虽没直接调用 mountReducer,但是几乎大同小异(区别只是 mountState 将 reducer 设置为basicStateReducer)

注:这里仅是 react 源码,至于重新渲染这块 react-dom 还没有去深入了解。

更新:

分两种情况,是否是 reRender,所谓reRender就是说在当前更新周期中又产生了新的更新,就继续执行这些更新知道当前渲染周期中没有更新为止

他们基本的操作是一致的,就是根据 reducerupdate.action 来创建新的 state,并赋值给Hook.memoizedState 以及 Hook.baseState

注意这里,对于非reRender得情况,我们会对每个更新判断其优先级,如果不是当前整体更新优先级内得更新会跳过,第一个跳过得Update会变成新的baseUpdate他记录了在之后所有得Update,即便是优先级比他高得,因为在他被执行得时候,需要保证后续的更新要在他更新之后的基础上再次执行,因为结果可能会不一样。

来源

preact 中的 hooks

Preact 最优质的开源 React 替代品!(轻量级 3kb)

注意:这里的替代是指如果不用 react 的话,可以使用这个。而不是取代。

useState 源码解析

调用了 useReducer 源码

export function useState(initialState) {
	return useReducer(invokeOrReturn, initialState);
}

useReducer 源码解析

// 模块全局定义
/** @type {number} */
let currentIndex; // 状态的索引,也就是前面模拟实现 useState 时候所说的指针

let currentComponent; // 当前的组件

export function useReducer(reducer, initialState, init) {
	/** @type {import('./internal').ReducerHookState} */
    // 通过 getHookState 方法来获取 hooks 
	const hookState = getHookState(currentIndex++);

	// 如果没有组件 也就是初始渲染
	if (!hookState._component) {
		hookState._component = currentComponent;
		hookState._value = [
			// 没有 init 执行 invokeOrReturn
				// invokeOrReturn 方法判断 initialState 是否是函数
				// 是函数 initialState(null) 因为初始化没有值默认为null
				// 不是函数 直接返回 initialState
			!init ? invokeOrReturn(null, initialState) : init(initialState),

			action => {
				// reducer == invokeOrReturn
				const nextValue = reducer(hookState._value[0], action);
				// 如果当前的值,不等于 下一个值
				// 也就是更新的状态的值,不等于之前的状态的值
				if (hookState._value[0]!==nextValue) {
					// 储存最新的状态
					hookState._value[0] = nextValue;
					// 渲染组件
					hookState._component.setState({});
				}
			}
		];
	}
    // hookState._value 数据格式也就是 [satea:any, action:Function] 的数据格式拉
	return hookState._value;
}

getHookState 方法

function getHookState(index) {
	if (options._hook) options._hook(currentComponent);
	const hooks = currentComponent.__hooks || (currentComponent.__hooks = { _list: [], _pendingEffects: [], _pendingLayoutEffects: [] });

	if (index >= hooks._list.length) {
		hooks._list.push({});
	}
	return hooks._list[index];
}

invokeOrReturn 方法

function invokeOrReturn(arg, f) {
	return typeof f === 'function' ? f(arg) : f;
}

总结

使用 hooks 几个月了。基本上所有类组件我都使用函数式组件来写。现在 react 社区的很多组件,都也开始支持hooks。大概了解了点重要的源码,做到知其然也知其所以然,那么在实际工作中使用他可以减少不必要的 bug,提高效率。

最后

全文章,如有错误或不严谨的地方,请务必给予指正,谢谢!

参考:

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.