karl

karl 查看完整档案

南京编辑东南大学  |  信息与通信工程 编辑  |  填写所在公司/组织 segmentfault.com/u/zf/about 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

karl 发布了文章 · 3月27日

react为什么使用fiber链表结构去遍历组件树

react为什么使用fiber链表结构去遍历组件树

1. 背景介绍

react fiber架构有两个主要的阶段:reconciliation/render 和 commit。在render阶段,react遍历整个组件树执行了以下操作:

  • 更新state和props
  • 调用生命周期钩子函数
  • 遍历组件的子元素,与之前的子元素进行比较,得到需要进行的DOM更新

如果react同步的遍历整个组件树,执行上述操作,可能会执行超过16ms(如果屏幕帧率60HZ),阻塞UI渲染,造成动画掉帧,出现视觉上的卡顿等。

所以应该怎么办呢?

浏览器幕后任务协作调度 APIrequestIdeleCallback在浏览器的空闲时段内调用的函数排队,使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。

如果我们使用一个performWork函数来执行React对组件树的整个操作,并使用requestIdleCallback API来对该任务进行调度,那么我们的代码逻辑可能是这样子:我们对一个组件进行处理并返回下一个组件等待调度,这样我们就不用像React16前的版本那样,同步的遍历整个组件树。

但是我们需要一种将render中的组件树的遍历过程分解为一个个增量单元的方法,即可以在完成某个组件的reconciliation之后,将调度权交还给浏览器以执行高优先级的用户交互、UI渲染等操作,待浏览器空闲时再继续下一个组件的reconciliation。 如果继续使用之前的组件树形结构,如下图所示,我们只能用递归的方式去实现组件树的遍历。

components_tree.png

// 深度优先遍历组件树
const root = document.getElementById('root');
function logName(node){
  console.log(node.dataset.name)
}
function traversalTree(root){
  logName(root);
  const childNodes = root.childNodes;
  for(let childNode of childNodes){
    if(childNode.nodeType !== 3){
      traversalTree(childNode);
    } 
  }
}
traversalTree(root);

//输出
a1, b1, b2, c1, d1, d2, b3, c2

递归方法非常适用于遍历树形结构,如上所示,但是递归模型无法做到增量渲染,也不能实现暂停某个组件的渲染并在浏览器空闲的时候继续执行。所以React采用了基于链表的Fiber模型

2. Fiber链表遍历过程

Fiber链表结构遍历需要以下三个字段:

  • child——指向第一个子节点
  • sibiling——指向第一个兄弟节点
  • return——指向父节点

上文中的组件树对应的Fiber结构如下所示:

fiberTree.png

遍历fiber链表的过程如下所示:

function workLoop(){
  while(nextUnitOfWork  && !shouldYield()){
    nextUnitOfWork = performUnitWork(nextUnitOfWork)
  }
}
const root = rootFiber;
function performUnitWork(node){
  //  这里对该节点执行render流程
        let child = perWorkOfNode(node)
    // 如果有子节点,继续遍历子节点
    if(child){
      return child;
    }
          // 如果回到了根节点,表示Fiber链表遍历完成
    if(node === root){
      return null
    }

    //如果没有子节点,也没有兄弟节点,则回父节点,如果父节点依然没有兄弟节点,则回到更上一层节点
    while(!node.sibling){
      if(!node.return || node.return === root){
        return nill
      }
      node = node.return;
    }
   
    return node.sibling;
}

上述算法使用nextUnitOfWork变量保存对当前Fiber节点的引用,能够异步的遍历组件树对应的每个Fiber节点,用requestIdleCallback包裹workLoop,使用shouldYield来检查是否有剩余时间执行nextUnitOfWork,如果没有剩余时间,则将控制权交还给浏览器,等待下一次调度从中断的nextUnitOfWork继续执行。

3. 从React elements 到 Fiber Nodes

我们从一个 React classComponent开始分析从React elements 到 Fiber Nodes的转化过程。

class ClickCounter extends React.Componenet{
  constructor(props) {
        super(props);
        this.state = {count: 0};
        this.handleClick = this.handleClick.bind(this);
    }

  handleClick() {
    this.setState((state) => {
      return {count: state.count + 1};
    });
  }
  render(){
    return [
      <button    key='1' onClick={this.handleClick}>Update counter</button>
            <span key='2'>{this.state.count}</span>    
    ]
  }
}

// babel jsx转化 =>

    React.createElement(
      'button',
    {
      key:'1',
      onClick: this.onClick
    },
    'Update counter'
  ),
  React.createElement(
      'span',
    {
      key:'2'
    },
    this.state.count
  )

// createElement执行得到react elements
[
   {
     $$typeof: Symbol(react.element),
     type: 'button',
     key: '1',
     props: {
       children: 'Update counter',
       onClick: () => { ... }
     }
   },
   {
     $$typeof: Symbol(react.element),
     type: 'span',
     key: "2",
     props: {
       children: 0
     }
    }
]

每个react element都有一个对应的fiber node,我们可以把fiber node看作一种描述unitWork的对象,这些对象按照链表的方式链接起来,可以方便的进行调度执行。ClickCounter组件fiber node的简略版的数据结构如下:

{
    return: null,
    child: null,
    sibling: null,
    stateNode: new ClickCounter,
    type: ClickCounter,
    tag: 1,
    alternate: null,
    key: null,
    updateQueue: null,
    memoizedProps: {},
    pendingProps: {},
    effectTag: 0,
    nextEffect: null  
}

stateNode保存类组件的实例,HostComponent的DOM节点或者其他React元素类型的类实例的引用。

type保存类组件的构造函数,或者HostComponent的Html标签,或者函数组件。

tag定义fiber的类型,例如0表示FunctionComponent,1表示ClassComponent,详情见此处

alternate指向workInProgress上的对应fiber node,current <==> workInProgress

key 是多个子组件的唯一标识,在组件更新时便于复用

updateQueue队列结构,保存状态更新、回调函数、DOM更新等

memoizedProps保存上一次渲染的props

pendingProps保存nextProps

effectTag标记该节点在commit阶段需要执行的副作用类型

nextEffect单链表用来快速查找下一个side effect

参考资料

Inside Fiber: in-depth overview of the new reconciliation algorithm in React

In-depth explanation of state and props update in React

查看原文

赞 1 收藏 1 评论 0

karl 发布了文章 · 3月21日

redux异步action解决方案

redux异步action解决方案

如果没有中间件,store.dispatch只能接收一个普通对象作为action。在处理异步action时,我们需要在异步回调或者promise函数then内,async函数await之后dispatch。

dispatch({
    type:'before-load'
})
fetch('http://myapi.com/${userId}').then({
    response =>dispatch({
            type:'load',
            payload:response
        })
})

这样做确实可以解决问题,特别是在小型项目中,高效,可读性强。 但缺点是需要在组件中写大量的异步逻辑代码,不能将异步过程(例如异步获取数据)与dispatch抽象出来进行复用。而采用类似redux-thunk之类的中间件可以使得dispatch能够接收不仅仅是普通对象作为action。例如:

function load(userId){
    return function(dispatch,getState){
        dispatch({
            type:'before-load'
        })
        fetch('http://myapi.com/${userId}').then({
            response =>dispatch({
                type:'load',
                payload:response
            })
        })
    }    
}
//使用方式
dispatch(load(userId))

使用中间件可以让你采用自己方便的方式dispatch异步action,下面介绍常见的三种。

1. redux-thunk


function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

2. redux-promise

使用redux-promise可以将action或者action的payload写成promise形式。 源码:

export default function promiseMiddleware({ dispatch }) {
  return next => action => {
    if (!isFSA(action)) {
      return isPromise(action) ? action.then(dispatch) : next(action);
    }

    return isPromise(action.payload)
      ? action.payload
          .then(result => dispatch({ ...action, payload: result }))
          .catch(error => {
            dispatch({ ...action, payload: error, error: true });
            return Promise.reject(error);
          })
      : next(action);
  };
}

3. Redux-saga

redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易

3.1 基本使用

import { call, put, takeEvery, takeLatest} from 'redux-saga/effects';

//复杂的异步流程操作
function* fetchUser(action){
  try{
    const user = yield call(API.fetchUser, action.payload);
    yield put({type:"USER_FETCH_SUCCEEDED",user:user})
  }catch(e){
    yield put({type:"USER_FETCH_FAILED",message:e.message})
  }
}

//监听dispatch,调用相应函数进行处理
function* mainSaga(){
  yield takeEvery("USER_FETCH_REQUESTED",fetchUser);
}

//在store中注入saga中间件
import {createStore,applyMiddleware} from 'redux';
import createSagaMiddleware from 'redux-saga';

import reducer from './reducers';
import mainSaga from './mainSaga';
const sagaMiddleware = createSagaMiddleware();

const store = createStore(reducer,initalState,applyMiddleware(sagaMiddleware));

sagaMiddleware.run(mainSaga)

3.2 声明式effects,便于测试

为了测试方便,在generator中不立即执行异步调用,而是使用call、apply等effects创建一条描述函数调用的对象,saga中间件确保执行函数调用并在响应被resolve时恢复generator。

function* fetchProducts() {
  const products = yield Api.fetch('/products')
  dispatch({ type: 'PRODUCTS_RECEIVED', products })
}

//便于测试
function* fetchProducts() {
  const products = yield call(Api.fetch, '/products')
  //便于测试dispatch
  yield put({ type: 'PRODUCTS_RECEIVED', products })
  // ...
}
// Effect -> 调用 Api.fetch 函数并传递 `./products` 作为参数
{
  CALL: {
    fn: Api.fetch,
    args: ['./products']  
  }
}

3.3 构建复杂的控制流

saga可以通过使用effect创建器、effect组合器、saga辅助函数来构建复杂的控制流。

effect创建器:

  • take:阻塞性effect,等待store中匹配的action或channel中的特定消息。
  • put:非阻塞性effect,用来命令 middleware 向 Store 发起一个 action。
  • call:阻塞性effect,用来命令 middleware 以参数 args 调用函数 fn
  • fork:非阻塞性effect,用来命令 middleware 以 非阻塞调用 的形式执行 fn
  • select:非阻塞性effect,用来命令 middleware 在当前 Store 的 state 上调用指定的选择器

effect组合器:

  • race:阻塞性effect:用来命令 middleware 在多个 Effect 间运行 竞赛(Race)

    function fetchUsersSaga {  
     const { response, cancel } = yield race({  
     response: call(fetchUsers),  
     cancel: take(CANCEL_FETCH)  
     })  
    }
  • all: 当 array 或 object 中有阻塞型 effect 的时候阻塞,用来命令 middleware 并行地运行多个 Effect,并等待它们全部完成

    function* mySaga() {  
     const [customers, products] = yield all([  
     call(fetchCustomers),  
     call(fetchProducts)  
     ])  
    }

effect辅助函数:

  • takeEvery:非阻塞性effect,在发起(dispatch)到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga

       const takeEvery = (patternOrChannel, saga, ...args) => fork(function*() {  
    while (true) {  
    const action = yield take(patternOrChannel)  
    yield fork(saga, ...args.concat(action))  
    }  
       })
  • takeLatest:非阻塞性,在发起到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。并自动取消之前所有已经启动但仍在执行中的 saga 任务。

      const takeLatest = (patternOrChannel, saga, ...args) => fork(function*() {  
       let lastTask  
       while (true) {  
       const action = yield take(patternOrChannel)  
       if (lastTask) {  
       yield cancel(lastTask) // 如果任务已经结束,cancel 则是空操作  
       }  
       lastTask = yield fork(saga, ...args.concat(action))  
       }  
      })
  • throttle:非阻塞性,在 ms 毫秒内将暂停派生新的任务

       const throttle = (ms, pattern, task, ...args) => fork(function*() {  
        const throttleChannel = yield actionChannel(pattern)  
       ​  
        while (true) {  
        const action = yield take(throttleChannel)  
        yield fork(task, ...args, action)  
        yield delay(ms)  
        } 
       })
查看原文

赞 2 收藏 1 评论 0

karl 发布了文章 · 3月14日

React Scheduler

React Scheduler

scheduler.png

1. unstable_scheduleCallback

react调度过程的入口是unstable_scheduleCallback函数,该函数接收调度任务的优先级priorityLevel、任务函数callback以及option三个参数。priorityLevel用来计算任务的timeout时间,对应关系如下:

ImmediatePriority => IMMEDIATE_PRIORITY_TIMEOUT : -1
UserBlockingPriority => USER_BLOCKING_PRIORITY : 250
IdlePriority => IDLE_PRIORITY : 5000
LowPriority => LOW_PRIORITY_TIMEOUT : 10000
NormalPriority => NORMAL_PRIORITY_TIMEOUT : maxSigned31BitInt

option参数包含delay和timeout选项,分别决定任务的startTime和timout,这两者相加得到任务的过期时间(expirationTime)。unstable_scheduleCallback中新建的newTask结构如下:

var newTask = {  
 id: taskIdCounter++,  
 callback,  
 priorityLevel,  
 startTime,  
 expirationTime,  
 sortIndex: -1,  
 };

接下来将newTask按startTime的区别推入最小堆timeQueue或者taskQueue,开始调度过程(最小堆结构可以方便的从中取出优先级最高的任务)。

2. delayed task的处理

当task的startTime < currentTime时,说明这是一个延时任务,将该任务推入timeQueue,timeQueue是按startTime排列的最小堆结构。推入任务后,如果taskQueue为空且这个新推入的延时任务是timeQueue中的第一个任务,则到该任务的startTime时,调用handleTimeout(currentTime)将timeQueue中startTime >= currentTime的task推入taskQueue。

if (startTime > currentTime) {
    // This is a delayed task.
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // Schedule a timeout.
      //startTime - currentTime秒后执行handleTimeout,将timeQueue中的task推入taskQueue
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  }

3. taskQueue中task的调度过程

unstable_scheduleCallback如果新建的是一个非延时任务,则按照expirationTime排序推入最小堆taskQueue,接着调用requestHostCallback(flushWork)请求调度。

newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
// Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
  isHostCallbackScheduled = true;
  requestHostCallback(flushWork);
}

flushWork中主要是改变一些状态变量,接下来进入workLoop。

isHostCallbackScheduled = false;
 if (isHostTimeoutScheduled) {
    // We scheduled a timeout but it's no longer needed. Cancel it.
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }
  isPerformingWork = true;
  try{
   return workLoop(hasTimeRemaining, initialTime);
  }catch{
      currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
  }

workLoop函数通过while循环执行taskQueue中的任务,当当前优先级最高的任务的expiraTime < currentTime并且当前时间片用完时,跳出workLoop循环,如果当前taskQueue中有更多的任务,则返回true等待下一次调度,否则返回false;

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    // This currentTask hasn't expired, and we've reached the deadline.
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      break;
    }
    const callback = currentTask.callback;
    if (callback !== null) {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        currentTask.callback = continuationCallback;
      } else {
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      advanceTimers(currentTime);
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }
  // Return whether there's additional work
  if (currentTask !== null) {
    return true;
  } else {
    let firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

4 requestHostCallback

flushWork相当于将taskQueue中的任务包裹起来供requestHostCallback执行,真正对taskQueue中任务的分片执行逻辑在requestHostCallback中。

requestHostCallback = function(callback) {  
     scheduledHostCallback = callback;  
     if (!isMessageLoopRunning) {  
         isMessageLoopRunning = true;  
         port.postMessage(null);  
     }  
 };

flushWork传人后保存在全局变量scheduledHostCallback中,postMessage方法会创建一个宏任务,且给这个宏任务分配的默认执行时间是yieldInterval=5ms, 这样每执行5ms就有机会将控制权交还给浏览器,从而不阻塞浏览器的UI渲染task,以及一些高优先级任务的入堆,使得即使在帧率较高的浏览器上也能保持较好的反应速度。
宏任务的执行在postMessage的onMessage回调中:

channel.port1.onmessage = performWorkUntilDeadline;
const performWorkUntilDeadline = () => {
    if (scheduledHostCallback !== null) {
      const currentTime = getCurrentTime();
      // Yield after `yieldInterval` ms, regardless of where we are in the vsync
      // cycle. This means there's always time remaining at the beginning of
      // the message event.
      deadline = currentTime + yieldInterval;
      const hasTimeRemaining = true;
      try {
        const hasMoreWork = scheduledHostCallback(
          hasTimeRemaining,
          currentTime,
        );
        if (!hasMoreWork) {
          isMessageLoopRunning = false;
          scheduledHostCallback = null;
        } else {
          // If there's more work, schedule the next message event at the end of the preceding one.
          port.postMessage(null);
        }
      } catch (error) {
        // If a scheduler task throws, exit the current browser task so the
        // error can be observed.
        port.postMessage(null);
        throw error;
      }
    } else {
      isMessageLoopRunning = false;
    }
    // Yielding to the browser will give it a chance to paint, so we can
    // reset this.
    needsPaint = false;
  };
查看原文

赞 0 收藏 0 评论 0

karl 发布了文章 · 3月10日

React-redux总结

React-redux总结

1.Provider

function Provider({store, context, children}){
    //contextValue初始化
    const contextValue = useMemo(()=>{
        const subscription = new Subscription(store);
        subscription.onStateChange = subscription.notifyNestedSubs;
      //自定义订阅模型供子组件订阅
        return {
            store,
            subscription
        }
    }, [store])
    const previosState = useMemo(()=> store.getState(),[store]);
    useEffect(()=>{
        const { subscription } = contextValue;
      //根节点订阅原始store,子节点订阅父组件中的subscription
        subscription.trySubscribe();
        if(previosState !== store.getState()){
          //订阅模型形成网状结构,保证订阅函数的执行顺序为父节点到子节点的网状结构,防止子节点的订阅者先触发导致过期props问题。
            subscription.notifyNestedSubs();
        }
        return ()=>{
            subscription.tryUnsubscribe();
            subscription.onStateChange = null;
        }
    },[contextValue, previosState])
    const Context =context || ReactReduxContext;
    return <Context.Provider value={contextValue}>{children}</Context.Provider>
}

上面的逻辑可以简述如下:

  • 新建一个订阅模型subscription供子组件订阅
  • 在useEffect中subscription调用subscription.trySubscribe()订阅父组件传来的store。
  • 当store由于dispatch触发订阅函数时,执行subscription.notifyNestedSubs,由于子组件订阅的是父组件的subscription,子组件触发订阅函数

    ................

  • 将改造后的store通过contextAPI传入子组件。

2. Connect

connect可以将store中的数据和方法进行处理后通过props传入其包裹的组件中。

2.1 mapStateToProps: (state, ownProps) => stateProps

mapStateToProps方法主要用来对store中数据进行reshape。因为每次store变化都会触发所有connect中的mapStateToProps函数执行,所以该函数应该运行的足够快以免影响性能。必要的时候可以使用selector库来避免不必要的计算(相同输入的情况下不进行再次计算,而是使用上一次的缓存值)。

react-redux进行了很多优化以避免不必要的重复渲染,

(state) => stateProps(state, ownProps) => stateProps
mapStateToProps 执行条件:store state 变化store state 变化或者ownProps变化
组件重新渲染条件:stateProps变化stateProps变化或者ownProps变化

从上表可以总结一些tips:

  • 判断stateProps变化是采用shallow equality checks比较的, 每次执行(state, ownProps) => stateProps即使输入值一样,如果 stateProps中的每个field返回了新对象,也会触发重新渲染。可以使用selector缓存上一次的值来避免stateProps变化。
  • 当store state没有变化时,mapStateToProps不会执行。connect在每次dispatch后,都会调用store.getState()获取最新的state,并使用lastState === currentState判断是否变化。而且在 redux combineReducers API中也做了优化,即当reducer中state没有变化时,返回原来的state。
  • ownProps也是mapStateToProps执行和组件重新渲染的条件,所以能不传的时候不要传。

2.2 mapDispatchToProps

mapDispatchToProps可以将store.dispatch或者dispatch一个action的方法((…args) => dispatch(actionCreator(…args)))传递到其包裹的组件中。mapDispatchToProps有多种用法:

  • 不传递mapDispatchToProps时,将会将dispatch方法传递到其包裹的组件中。
  • mapDispatchToProps定义为函数时,(dispatch,ownProps)=>dispatchProps

    const increment = () => ({ type: 'INCREMENT' })
    const decrement = () => ({ type: 'DECREMENT' })
    const reset = () => ({ type: 'RESET' })
    
    const mapDispatchToProps = dispatch => {
      return {
        // dispatching actions returned by action creators
        increment: () => dispatch(increment()),
        decrement: () => dispatch(decrement()),
        reset: () => dispatch(reset())
      }
    }

    redux中提供了bindActionCreators接口可以自动的进行actionCreators到相应dispatch方法的转换:

    import { bindActionCreators } from 'redux'
    
    const increment = () => ({ type: 'INCREMENT' })
    const decrement = () => ({ type: 'DECREMENT' })
    const reset = () => ({ type: 'RESET' })
    
    function mapDispatchToProps(dispatch) {
      return bindActionCreators({ increment, decrement, reset }, dispatch)
    }
  • mapDispatchToProps定义为action creators键值对时,connect内部会自动调用bindActionCreators将其转化为dispatching action函数形式(dispatch => bindActionCreators(mapDispatchToProps, dispatch))。

3. batch

默认情况下,每次dispatch都会执行connect函数,并执行接下来可能的重复渲染过程,使用batchAPI,可以将多次dispatch合并,类似setState的合并过程。

import { batch } from 'react-redux'

function myThunk() {
  return (dispatch, getState) => {
    // should only result in one combined re-render, not two
    batch(() => {
      dispatch(increment())
      dispatch(increment())
    })
  }
}

4. hooks

可以在不用connect包裹组件的情况下订阅store或dispatch action。

4.1 useSelector()

下面是对useSelector的简单实现:

const useSelector = selector => {
  const store = React.useContext(Context);
  const [, forceUpdate] = React.useReducer(c => c + 1, 0);
  const currentState = React.useRef();
  // 在re-render阶段更新state使得获取的props不是过期的
  currentState.current = selector(store.getState());

  React.useEffect(() => {
    return store.subscribe(() => {
      try {
        const nextState = selector(store.getState());

        if (nextState === currentState.current) {
          // Bail out updates early
          return;
        }
      } catch (err) {
        // Ignore errors
        //忽略由于过期props带来的计算错误
      }
            //state变化需要重新渲染
      // Either way we want to force a re-render(与其他订阅者中的forceUpdate合并执行)
      forceUpdate();
    });
  }, [store, forceUpdate, selector, currentState]);

  return currentState.current;
};

useSelector中新旧state的对比使用===,而不是connect中的浅比较,所以selector返回一个新的对象会导致每次重新渲染。对于这个问题,可以使用多个selector返回基本类型的值来解决;或者使用reselect库缓存计算结果;最后还可以传递useSelector的第二个参数,自定义新旧state的比较函数。

connect中会对新旧state和props值进行比较来决定是否执行mapStateToProps,但是useSelector中没有这种机制,所以不会阻止由于父组件的re-render导致的re-render(即使props不变化),这种情况下可以采用React.memo优化。

4.2 useDispatch()

返回dispatch的引用

const dispatch = useDispatch()

4.3 useStore()

返回store的引用

const store = useStore()
查看原文

赞 0 收藏 0 评论 1

karl 发布了文章 · 2019-10-17

前端路由库

前端路由的原理大致相同:当页面的URL发生变化时,页面的显示结果可以根据URL的变化而变化,但是页面不会刷新。

要实现URL变化页面不刷新有两种方法:通过hash实现、通过History API实现。

1. 实现方法

  • hash实现原理

    改变页面的hash值不会刷新页面,而hashchange的事件,可以监听hash的变化,从而在hash变化时渲染新页面。

  • History API实现原理

    History API中pushState、replaceState方法会改变当前页面url,但是不会伴随着刷新,但是调用这两个方法改变页面url没有事件可以监听。有个history库增强了history API,采用发布订阅模式来对url的变化作出反映。其暴露出一个listen方法来添加订阅者,通过重写push、replace方法,使得这两个方法调用时通知订阅者,从而在url变化时渲染新页面。

2. react-route库

2.1 基本结构

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

export default function App() {
  return (
    <Router>
      
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/about">About</Link>
            </li>
            <li>
              <Link to="/users">Users</Link>
            </li>
          </ul>
        </nav>
        
        <Switch>
          <Route path="/about">
            <About />
          </Route>
          <Route path="/users">
            <Users />
          </Route>
          <Route path="/">
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

function Home() {
  return <h2>Home</h2>;
}

function About() {
  return <h2>About</h2>;
}

function Users() {
  return <h2>Users</h2>;
}

react-router使用的基本结构是:

  1. 外层使用<Router>包裹整个app,主要类型有<BrowserRouter><HashRouter>,分别对应上面两种实现方法;首先把location、history对象(增强的)通过react context API注入到子组件中,然后在<Router>中会调用history.listen方法监听location变化,当location变化时采用setState改变location触发子组件的更新。
  2. <Link>标签做导航用,点击时会调用history.pushhistory.replace方法,并改变context中的location。
  3. context变化导致<Switch>重新渲染,找到匹配的<Route>渲染。
  4. Route组件根据Swtich的匹配结果渲染component,并通过React context API将location、history对象注入到子组件。

2.2 StaticRouter

服务端渲染时页面是静态的,没有state,不能通过state改变去触发子组件更新。在服务端是根据req.url来渲染页面的,其基本使用方式如下:

import http from "http";
import React from "react";
import ReactDOMServer from "react-dom/server";
import { StaticRouter } from "react-router-dom";

import App from "./App.js";

http
  .createServer((req, res) => {
    const context = {};

    const html = ReactDOMServer.renderToString(
      <StaticRouter location={req.url} context={context}>
        <App />
      </StaticRouter>
    );
        //重定向时触发
    if (context.url) {
      res.writeHead(context.status, {
        Location: context.url
      });
      res.end();
    } else {
      res.write(`
      <!doctype html>
      <div id="app">${html}</div>
    `);
      res.end();
    }
  })
  .listen(3000);

​ 在<StaticRouter>中没有使用history库了,而是创建了一个简单的history对象,其对应history库创建的history对象,但是其中的方法大多数为空的,例如:

handleListen = () => {};

只是为了将history传递时不报错。其中的push和replace方法是有效的,调用时会给context.url、context.location赋值。如上所示,但context.url有值时会重定向。

由于<StaticRouter>内部会

return <Router {...rest} history={history} staticContext={context} />;

而statusContext属性在客户端渲染时不存在,可以通过这个条件去增加返回码:

<Route
      render={({ staticContext }) => {
        if (staticContext) staticContext.status = status;
        // Redirect会调用push或replace
        return <Redirect from={from} to={to} />;
      }}
    />

2.3 静态路由 React Router Config

import { renderRoutes } from "react-router-config";

const routes = [
  {
    component: Root,
    routes: [
      {
        path: "/",
        exact: true,
        component: Home
      },
      {
        path: "/child/:id",
        component: Child,
        routes: [
          {
            path: "/child/:id/grand-child",
            component: GrandChild
          }
        ]
      }
    ]
  }
];

const Root = ({ route }) => (
  <div>
    <h1>Root</h1>
    {/* child routes won't render without this */}
    {renderRoutes(route.routes)}
  </div>
);

const Home = ({ route }) => (
  <div>
    <h2>Home</h2>
  </div>
);

const Child = ({ route }) => (
  <div>
    <h2>Child</h2>
    {/* child routes won't render without this */}
    {renderRoutes(route.routes, { someProp: "these extra props are optional" })}
  </div>
);

const GrandChild = ({ someProp }) => (
  <div>
    <h3>Grand Child</h3>
    <div>{someProp}</div>
  </div>
);
//renderRoutes方法对routes进行map生成<Route>
ReactDOM.render(
  <BrowserRouter>
    {/* kick it all off with the root route */}
    {renderRoutes(routes)}
  </BrowserRouter>,
  document.getElementById("root")
);

3. Universal Router库

Universal Router是一个轻量化的静态路由库,可以使用在客户端和服务端。

client端的处理:

  1. 引入history库,通过history.location获得当前location并进行初始渲染。
  2. 调用history.listen监听url变化,url变化时触发重新渲染函数。
  3. 渲染函数中首先得到location.pathname,调用router.resolve({pathname})得到匹配的route,最后调用render方法进行渲染。

server端的处理:

  1. 服务端没有url状态的变化,可以直接从req.path的的得到路由信息
  2. 调用router.resolve({pathname})得到匹配的route,最后调用render方法进行渲染。

路由配置代码的基本结构:

const routes = [
  { path: '/one', action: () => '<h1>Page One</h1>' },
  { path: '/two', action: () => '<h1>Page Two</h1>' },
  { path: '(.*)', action: () => '<h1>Not Found</h1>' }
]

//context this.context = { router: this, ...options.context }
const router = new UniversalRouter(routes, {context,resolveRoute})

//resolve的参数pathnameOrContext
// const context = {
//      ...this.context,
//      ...(typeof pathnameOrContext === 'string'
//        ? { pathname: pathnameOrContext }
//        : pathnameOrContext),
//    }
router.resolve({ pathname: '/one' }).then(result => {
  document.body.innerHTML = result
  // renders: <h1>Page One</h1>
})
  • 首先通过routes定义静态路由,path属性是必须的,action是resolve时默认的调用函数

     function resolveRoute(context, params) {
       if (typeof context.route.action === 'function') {
         return context.route.action(context, params)
       }
       return undefined
     }
  • 生成router实例,此时可以通过resolveRoute option定义router.resolve时的逻辑,通过context添加自定义的方法和属性。
  • 调用router.resolve去匹配pathname,该函数的参数都会加到context属性上,函数内部返回resolveRoute(context, params)的返回值。

权限管理:

  • context对象上有next方法,调用context.next()会遍历resolve其子路由,调用context.next(true)会遍历resolve所有剩余路由。
  • resolve得到的返回值为undefined时将会尝试匹配其子路由,得到的返回值为null时将会尝试匹配其兄弟路由
const middlewareRoute = {
  path: '/admin',
  action(context) {
    if (!context.user) {
      return null // route does not match (skip all /admin* routes)
    }
    if (context.user.role !== 'Admin') {
      return 'Access denied!' // return a page (for any /admin* urls)
    }
    return undefined // or `return context.next()` - try to match child routes
  },
  children: [/* admin routes here */],
}
查看原文

赞 0 收藏 0 评论 0

karl 发布了文章 · 2019-09-02

webpack源码阅读之主流程分析

webpack源码阅读之主流程分析

图片描述

comipler是其webpack的支柱模块,其继承于Tapable类,在compiler上定义了很多钩子函数,贯穿其整个编译流程,这些钩子上注册了很多插件,用于在特定的时机执行特定的操作,同时,用户也可以在这些钩子上注册自定义的插件来进行功能拓展,接下来将围绕这些钩子函数来分析webpack的主流程。

1. compiler实例化

compiler对象的生成过程大致可以简化为如下过程,首先对我们传入的配置进行格式验证,接着调用Compiler构造函数生成compiler实例,自定义的plugins注册,最后调用new WebpackOptionsApply().process(options, compiler)进行默认插件的注册,comailer初始化等。

const webpack = (options,callback)=>{
    //options格式验证
  const webpackOptionsValidationErrors = validateSchema(
        webpackOptionsSchema,
        options
    );
  ...
  //生成compiler对象
    let compiler = new Compiler(options.context);
  
  //自定义插件注册
  if (options.plugins && Array.isArray(options.plugins)) {
            for (const plugin of options.plugins) {
                if (typeof plugin === "function") {
                    plugin.call(compiler, compiler);
                } else {
                    plugin.apply(compiler);
                }
            }
        }
  
  //默认插件注册,默认配置等
  compiler.options = new WebpackOptionsApply().process(options, compiler);
}

Webpackoprionapply是一个重要的步骤,通常是此处插件注册在compiler.hooks.thisCompilation或compiler.hooks.compilation上,并在compilation钩子上调用时,进一步注册到parser(用于生成依赖及依赖模版)或者mainTemplate(用于seal阶段render)的钩子上:

process(options, compiler) {
  //当target是一个函数时,可以自定义该环境下使用哪些plugins
        if (typeof options.target === "string") {
//1.不同target下引入不同的plugin进行文件加载
            switch (options.target) {
                case "web":
          //JsonpTemplatePlugin插件注册在compiler.hooks.this.compilation上,并在该钩子调用时,在compilation.mainTemplate的多个钩子上注册事件以在最后生成的代码中加入Jsonp Script进行文件加载
                    new JsonpTemplatePlugin().apply(compiler);
                    new FetchCompileWasmTemplatePlugin({
                        mangleImports: options.optimization.mangleWasmImports
                    }).apply(compiler);
          //在compiler.hooks.compilation上注册,并挂载在compilation.moduleTemplates.javascript上,在seal阶段template.hooks.render时调用
                    new FunctionModulePlugin().apply(compiler);
                    new NodeSourcePlugin(options.node).apply(compiler);
                    new LoaderTargetPlugin(options.target).apply(compiler);
                    break;
                case "node":
                case "async-node":
          //如果目标环境为node,可以用require方式加载文件,而不需要使用Jsonp
                    new NodeTemplatePlugin({
                        asyncChunkLoading: options.target === "async-node"
                    }).apply(compiler);
                    new ReadFileCompileWasmTemplatePlugin({
                        mangleImports: options.optimization.mangleWasmImports
                    }).apply(compiler);
                    new FunctionModulePlugin().apply(compiler);
                    new NodeTargetPlugin().apply(compiler);
                    new LoaderTargetPlugin("node").apply(compiler);
                    break;
                ...........
        }
//2. output Library处理
          ...........
//3. devtool sourceMap处理
        ...........
//注册在compiler.hooks.compilation上,给normalModuleFactory的js模块提供Parser、JavascriptGenerator对象 ,并给seal阶段的template提供renderManifest数组(包含render方法)           
        new JavascriptModulesPlugin().apply(compiler);
//注册在compiler.hooks.compilation上,给normalModuleFactory的jso n模块提供Parser、JavascriptGenerator对象      
        new JsonModulesPlugin().apply(compiler);
//同理,webassembly模块      
        new WebAssemblyModulesPlugin({
            mangleImports: options.optimization.mangleWasmImports
        }).apply(compiler);

//4. 入口不同格式下的处理,注册在compiler.hooks.entryOption,在调用时新建SingleEntryPlugin或MultiEntryPlugin 
        new EntryOptionPlugin().apply(compiler);
        compiler.hooks.entryOption.call(options.context, options.entry);

//5. 不同模块写法的处理,一般注册在compiler.hooks.compilation上,调用时在normalModuleFactory.hooks.parse上注册,接着在parse的hooks上注册,在parse阶段,遇到不同的节点调用不同的plugin,从而在模块的dependencies数组中推入不同的dependencyFactory和dependencyTemplate
        new CompatibilityPlugin().apply(compiler);
      //es模块
        new HarmonyModulesPlugin(options.module).apply(compiler);
        if (options.amd !== false) {
      //AMD模块
            const AMDPlugin = require("./dependencies/AMDPlugin");
            const RequireJsStuffPlugin = require("./RequireJsStuffPlugin");
            new AMDPlugin(options.module, options.amd || {}).apply(compiler);
            new RequireJsStuffPlugin().apply(compiler);
        }
      //CommonJS模块
        new CommonJsPlugin(options.module).apply(compiler);
        new LoaderPlugin().apply(compiler);
        if (options.node !== false) {
            const NodeStuffPlugin = require("./NodeStuffPlugin");
            new NodeStuffPlugin(options.node).apply(compiler);
        }
        new ImportPlugin(options.module).apply(compiler);
        new SystemPlugin(options.module).apply(compiler);
     .........
     
//6. 优化
     .........
        
//7. modeId、chunkId相关
     .........
     
//8. resolve初始配置,在resolve时调用this.getResolver时调用     
        compiler.resolverFactory.hooks.resolveOptions
            .for("normal")
            .tap("WebpackOptionsApply", resolveOptions => {
                return Object.assign(
                    {
                        fileSystem: compiler.inputFileSystem
                    },
                    cachedCleverMerge(options.resolve, resolveOptions)
                );
            });
        compiler.resolverFactory.hooks.resolveOptions
            .for("context")
            .tap("WebpackOptionsApply", resolveOptions => {
                return Object.assign(
                    {
                        fileSystem: compiler.inputFileSystem,
                        resolveToContext: true
                    },
                    cachedCleverMerge(options.resolve, resolveOptions)
                );
            });
        compiler.resolverFactory.hooks.resolveOptions
            .for("loader")
            .tap("WebpackOptionsApply", resolveOptions => {
                return Object.assign(
                    {
                        fileSystem: compiler.inputFileSystem
                    },
                    cachedCleverMerge(options.resolveLoader, resolveOptions)
                );
            });
        compiler.hooks.afterResolvers.call(compiler);
        return options;
    }

2. compiler.run

生成compler实例后,cli.js中就会调用compiler.run方法了,compiler.run的流程大致可以简写如下(去掉错误处理等逻辑),其囊括了整个打包过程,首先依次触发beforeRun、run等钩子,接下来调用compiler.compile()进行编译过程,在回调中取得编译后的compilation对象,调用compiler.emitAssets()输出打包好的文件,最后触发done钩子。

run(){
  const onCompiled = (err, compilation) => {
    //打包输出
            this.emitAssets(compilation, err => {
                this.hooks.done.callAsync(stats)
        };
    // beforeRun => run => this.compile()                 
        this.hooks.beforeRun.callAsync(this, err => {
            this.hooks.run.callAsync(this, err => {
                this.readRecords(err => {
                    this.compile(onCompiled);
                });
            });
        });
}

3. compiler.compile

在这个方法中主要也是通过回调触发钩子进行流程控制,通过newCompilation=>make=>finsih=>seal流程来完成一次编译过程,compiler将具体一次编译过程放在了compilation实例上,可以将主流程与编译过程分割开来,当处于watch模式时,可以进行多次编译。

compile(callback) {
        const params = this.newCompilationParams();
        this.hooks.beforeCompile.callAsync(params, err => {
            this.hooks.compile.call(params);
            const compilation = this.newCompilation(params);
            this.hooks.make.callAsync(compilation, err => {
                compilation.finish(err => {
                    compilation.seal(err => {
                        this.hooks.afterCompile.callAsync(compilation, err => {
                            return callback(null, compilation);
                        });
                    });
                });
            });
        });
    }

从图中可以看到make钩子上注册了singleEntryPlugin(单入口配置时),compilation作为参数传入该插件,接着在插件中调用compilation.addEntry方法开始编译过程。

compiler.hooks.make.tapAsync(
            "SingleEntryPlugin",
            (compilation, callback) => {
                const { entry, name, context } = this;

                const dep = SingleEntryPlugin.createDependency(entry, name);
                compilation.addEntry(context, dep, name, callback);
            }
        );

4. compilation过程

编译过程的入口在compilation._addModuleChain函数,传入entry,context参数,在回调中得到编译生成的module。编译的过程包括文件和loader路径的resolve,loader对源文件的处理,递归的进行依赖处理等等.

addEntry(context, entry, name, callback) {
        this.hooks.addEntry.call(entry, name);
        this._addModuleChain(
            context,
            entry,
            module => {
                this.entries.push(module);
            },
            (err, module) => {
                this.hooks.succeedEntry.call(entry, name, module);
                return callback(null, module);
            }
        );
    }

this._addModuleChain中调用moduleFactory.create()来开始模块的创建,模块创建第一步需要通过resolve得到入口文件的具体路径。

4.1 resolve

webpack 中每涉及到一个文件,就会经过 resolve 的过程。webpack 使用 enhanced-resolve 来提供绝对路径、相对路径、模块路径的多样解析方式。

图片描述

moduleFactory.create()resolve过程从通过调用normalModuleFactory中factory函数开始。

factory(result, (err, module) => {
                    if (err) return callback(err);

                    if (module && this.cachePredicate(module)) {
                        for (const d of dependencies) {
                            dependencyCache.set(d, module);
                        }
                    }

                    callback(null, module);
                });

//传入的result的基本形式如下
result = {
  context: "/Users/hahaha/project/demo/webpack-demo"
    contextInfo: {issuer: "", compiler: undefined}
    dependencies: [SingleEntryDependency]
    request: "./src/index.js"
    resolveOptions: {}
}

factory方法拿到入口信息result后,将result传递给resolver方法,resolver方法先得到对普通文件和loader文件的resolve方法:

const loaderResolver = this.getResolver("loader");
const normalResolver = this.getResolver("normal", data.resolveOptions);

然后检查路径中是否包含内联loaders, 通过调用loaderResolver和normalResolver并行的resolve文件路径和内联loaders路径。如果使用了内联loaders,则将其保存在loaders变量中,接着对得到的文件路径进行ruler匹配,得到匹配到的loader数值:

const result = this.ruleSet.exec({
                            resource: resourcePath,
                            realResource:
                                matchResource !== undefined
                                    ? resource.replace(/\?.*/, "")
                                    : resourcePath,
                            resourceQuery,
                            issuer: contextInfo.issuer,
                            compiler: contextInfo.compiler
                        });·

this.ruleSet是用户定义的loaders和默认loader的格式化的结果,通过其exec方法可以得到与资源文件匹配的loaders数组。

接下来并行的resolver这些loaders路径,并保存在loaders数组中;值得注意的是,在resolver钩子的回调中初始化了parser和generator对象:

parser: this.getParser(type, settings.parser),
generator: this.getGenerator(type, settings.generator),

parser的注册方法如下(webpackOptionapply中各种模块处理的插件就是这样注册的):

compiler.hooks.normalModuleFactory.tap('MyPlugin', factory => {
  factory.hooks.parser.for('javascript/auto').tap('MyPlugin', (parser, options) => {
    parser.hooks.someHook.tap(/* ... */);
  });
});

入口文件通过resolver后得到的结果类似如下(没使用loaders,所以为空数组):

{
  context: "/Users/hahaha/project/demo/webpack-demo"
  dependencies: [SingleEntryDependency]
  generator: JavascriptGenerator {}
  loaders: [] 
  matchResource: undefined
  parser: Parser {_pluginCompat: SyncBailHook, hooks: {…}, options: {…}, sourceType: "auto", scope: undefined, …}
  rawRequest: "./src/index.js"
  request: "/Users/hahaha/project/demo/webpack-demo/src/index.js"
  resolveOptions: {}
  resource: "/Users/hahaha/project/demo/webpack-demo/src/index.js"
  resourceResolveData: {
    context: {…}, 
         path: "/Users/hahaha/project/demo/webpack-demo/src/index.js",
    request: undefined, 
    query: "",
    module: false, …
  }
  settings: {type: "javascript/auto", resolve: {…}}
  type: "javascript/auto"
  userRequest: "/Users/hahaha/project/demo/webpack-demo/src/index.js"
}

resolver过程得到的入口文件的路径,接下来在factory方法中会调用createdModule = new NormalModule(result)生成模块,改构造函数将resolver得到的信息保存到模块上,并提供了一些实例方法来进行后续的build过程。

本文并未深入resolve具体流程,详情可以参阅:

webpack系列之三resolve

4.2 build

图片描述

moduleFactory.create()方法的回调中得到resolve后生成的module后,将开始模块的build过程,我将代码主干保留如下:

moduleFactory.create(
                {
                    contextInfo: {
                        issuer: "",
                        compiler: this.compiler.name
                    },
                    context: context,
                    dependencies: [dependency]
                },
                (err, module) => {
                    const afterBuild = () => {
                        if (addModuleResult.dependencies) {
                            this.processModuleDependencies(module, err => {
                                if (err) return callback(err);
                                callback(null, module);
                            });
                        } else {
                            return callback(null, module);
                        }
                    };
                        this.buildModule(module, false, null, null, err =>                             {
                            afterBuild();
                        });
                
                }
            );

首先调用this.buildModule方法,由于moduleFactory.create()生成的module是normalModule(本例中)的实例,所以可以实际上是调用normalModule.doBuild()进行build,可以看到首先生成了一个loaderContext对象,在后面运行loader的时候,会通过call方法将loader的this指向loaderContext。

doBuild(options, compilation, resolver, fs, callback) {
        const loaderContext = this.createLoaderContext(
            resolver,
            options,
            compilation,
            fs
        );

        runLoaders(
            {
                resource: this.resource,
                loaders: this.loaders,
                context: loaderContext,
                readResource: fs.readFile.bind(fs)
            },
            (err, result) => {
                ...
                return callback();
            }
        );
    }

接下来就进入runloaders方法了,传入的参数包括模块路径,模块的loaders数组,loaderContext等。在该方法内,首先对相关参数进行初始化的操作,特别是将 loaderContext 上的部分属性改写为 getter/setter 函数,这样在不同的 loader 执行的阶段可以动态的获取一些参数。接下来进入iteratePitchingLoaders方法:

function iteratePitchingLoaders(options, loaderContext, callback) {
    //当处理完最后一个loader的pitch后,倒序开始处理loader的normal方法
    if(loaderContext.loaderIndex >= loaderContext.loaders.length)
        return processResource(options, loaderContext, callback);

    var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

    // iterate
    if(currentLoaderObject.pitchExecuted) {
        loaderContext.loaderIndex++;
        return iteratePitchingLoaders(options, loaderContext, callback);
    }

    // 在loadLoader中,通过module = require(loader.path)加载loader,并将module上的normal、pitch、raw属性拷贝到loader对象上
    loadLoader(currentLoaderObject, function(err) {
        if(err) {
            loaderContext.cacheable(false);
            return callback(err);
        }
        var fn = currentLoaderObject.pitch;
        currentLoaderObject.pitchExecuted = true;
    //如果没有该loader上没有pitch,则跳到下一个loader的pitch
        if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);
        //在runSyncOrAsync内执行loader上的pitch函数
        runSyncOrAsync(
            fn,
            loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
            function(err) {
                if(err) return callback(err);
                var args = Array.prototype.slice.call(arguments, 1);
                if(args.length > 0) {
                    loaderContext.loaderIndex--;
                    iterateNormalLoaders(options, loaderContext, args, callback);
                } else {
                    iteratePitchingLoaders(options, loaderContext, callback);
                }
            }
        );
    });
}

在深入runSyncOrAsync函数之前,我们先来介绍下webpack官网上的loader API

  • 同步loader

    //当不返回map,meta时可以直接返回
    module.exports = function(content,map,meta){
        return someSyncOperation(content)
    }
    
    //返回多个参数时,要通过this.callback调用
    module.exports = function(content,map,meta){  this.callback(null,someSyncOperation(content),map,meta)
        return
    }    
  • 异步loader

    //对于异步loader,使用this.async来获取callback函数
    module.exports = function(content,map,meta){
      let callback = this.async();
     someAsyncOperation(content,function(err,result,sourceMap,meta){
        if(err) return callback(err);
        callback(null,result,sourceMap,meta);
      })
    }
    
    //promise写法
    module.exports = function(content){
      return new Promise(resolve =>{
        someAsyncOperation(content,(err,result)=>{
          if(err) resolve(err)
          resolve(null,result)
        })
      })
    }

了解了loader的写法后,我们在来看看loader的执行函数runSyncOrAsync

function runSyncOrAsync(fn, context, args, callback) {
    var isSync = true;
    var isDone = false;
    var isError = false; // internal error
    var reportedError = false;
    context.async = function async() {
        if(isDone) {
            if(reportedError) return; // ignore
            throw new Error("async(): The callback was already called.");
        }
        isSync = false;
        return innerCallback;
    };
    var innerCallback = context.callback = function() {
        if(isDone) {
            if(reportedError) return; // ignore
            throw new Error("callback(): The callback was already called.");
        }
        isDone = true;
        isSync = false;
        try {
            callback.apply(null, arguments);
        } catch(e) {
            isError = true;
            throw e;
        }
    };
    try {
        var result = (function LOADER_EXECUTION() {
            return fn.apply(context, args);
        }());
        if(isSync) {
            isDone = true;
            if(result === undefined)
                return callback();
            if(result && typeof result === "object" && typeof result.then === "function") {
                return result.then(function(r) {
                    callback(null, r);
                }, callback);
            }
            return callback(null, result);
        }
    } catch(e) {
        if(isError) throw e;
        if(isDone) {
            // loader is already "done", so we cannot use the callback function
            // for better debugging we print the error on the console
            if(typeof e === "object" && e.stack) console.error(e.stack);
            else console.error(e);
            return;
        }
        isDone = true;
        reportedError = true;
        callback(e);
    }

}

结合loader执行的各种写法,runSyncOrAsync的逻辑就很清晰了。

我们知道,loader上的方法有pitch和normal之分,它们都是用runSyncOrAsync执行的,执行顺序为:

//config中
use: [
          'a-loader',
          'b-loader',
          'c-loader'
        ]

//执行顺序
|- a-loader `pitch`
  |- b-loader `pitch`
    |- c-loader `pitch`
      |- requested module is picked up as a dependency
    |- c-loader normal execution
  |- b-loader normal execution
|- a-loader normal execution

loader的pitch方法一般写法如下:

//data对象保存在loaderContext对象的data属性中,可以用于在循环时,捕获和共享前面的信息。
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  data.value = 42;
};

//pitch中有返回值时,会跳过后续的pitch和内层的normal方法
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  if (someCondition()) {
    return 'module.exports = require(' + JSON.stringify('-!' + remainingRequest) + ');';
  }
};

例如在style-loader和css-loader一起使用时,先执行的style-loader的pitch方法,返回值如下:

var content = require("!!../node_modules/css-loader/dist/cjs.js!./style.css");

if (typeof content === 'string') {
  content = [[module.id, content, '']];
}

var options = {}

options.insert = "head";
options.singleton = false;

var update = require("!../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js")(content, options);

if (content.locals) {
  module.exports = content.locals;
}

由于有返回值,会跳过后续的style-loader的pitch方法、css-loader的pitch方法、css-loader的normal方法和css-loader的normal方法。然后在后续处理依赖时处理内联loader的时候再进行css-loader的处理。

loaders处理之后,得到处理后的文件的内容字符串保存在module的_source变量中,如何从这个字符串中得到依赖呢?这就需要对这个字符串进行处理了,在回调函数中this.parser.parse 方法被执行:

parse(source, initialState) {
        let ast;
        let comments;
        if (typeof source === "object" && source !== null) {
            ast = source;
            comments = source.comments;
        } else {
            comments = [];
            ast = Parser.parse(source, {
                sourceType: this.sourceType,
                onComment: comments
            });
        }

        const oldScope = this.scope;
        const oldState = this.state;
        const oldComments = this.comments;
        this.scope = {
            topLevelScope: true,
            inTry: false,
            inShorthand: false,
            isStrict: false,
            definitions: new StackedSetMap(),
            renames: new StackedSetMap()
        };
        const state = (this.state = initialState || {});
        this.comments = comments;
        if (this.hooks.program.call(ast, comments) === undefined) {
            this.detectStrictMode(ast.body);
            this.prewalkStatements(ast.body);
            this.blockPrewalkStatements(ast.body);
            this.walkStatements(ast.body);
        }
        this.scope = oldScope;
        this.state = oldState;
        this.comments = oldComments;
        return state;
    }

先调用Parse.parse方法得到AST,然后就是对这个树进行遍历了,流程为: program事件 -> detectStrictMode -> prewalkStatements -> walkStatements。这个过程会通过遍历AST的各个节点,从而触发不同的钩子函数,在这些钩子函数上会触发一些模块处理的方法(这些方法大多是在webpackOptionapply中注册到parser上的)给 module 增加很多 dependency 实例,每个 dependency 类都会有一个 template 方法,并且保存了原来代码中的字符位置 range,在最后生成打包后的文件时,会用 template 的结果替换 range 部分的内容。

所以最终得到的 dependency 不仅包含了文件中所有的依赖信息,还被用于最终生成打包代码时对原始内容的修改和替换,例如将 return 'sssss' + A替换为 return 'sssss' + _a_js__WEBPACK_IMPORTED_MODULE_0__["A"]

program 事件中,会触发两个 plugin 的回调:HarmonyDetectionParserPlugin 和 UseStrictPlugin

HarmonyDetectionParserPlugin中,如果代码中有 import 或者 export 或者类型为 javascript/esm,那么会增加了两个依赖:HarmonyCompatibilityDependency, HarmonyInitDependency 依赖。

UseStrictPlugin用来检测文件是否有 use strict,如果有,则增加一个 ConstDependency 依赖。

整个 parse 的过程关于依赖的部分,我们总结一下:

  1. 将 source 转为 AST(如果 source 是字符串类型)
  2. 遍历 AST,遇到 import 语句就增加相关依赖,代码中出现 A(import 导入的变量) 的地方也增加相关的依赖。

所有的依赖都被保存在 module.dependencies 中。module.dependencies大致内容如下:

0:CommonJsRequireDependency
  loc: SourceLocation
  end: Position {line: 1, column: 77}
  start: Position {line: 1, column: 14}
  __proto__: Object
  module: null
  optional: false
  range: (2) [22, 76]
  request: "!!../node_modules/css-loader/dist/cjs.js!./style.css"
  userRequest: "!!../node_modules/css-loader/dist/cjs.js!./style.css"
  weak: false
  type: (...)
1: RequireHeaderDependency {module: null, weak: false, optional: false, loc: SourceLocation, range: Array(2)}
2: ConstDependency {module: null, weak: false, optional: false, loc: SourceLocation, expression: "module.i", …}
3: CommonJsRequireDependency {module: null, weak: false, optional: false, loc: SourceLocation, request: "!../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js", …}
4: RequireHeaderDependency {module: null, weak: false, optional: false, loc: SourceLocation, range: Array(2)}

如图中所示,接下来就是处理依赖了,进入回调中的processModuleDependencies方法:

processModuleDependencies(module, callback) {
        const dependencies = new Map();    
    const addDependency = dep => {
        const resourceIdent = dep.getResourceIdentifier();
    // 过滤掉没有 ident 的,就是请求路径,例如 constDependency 这些只用在最后打包文件生成的依赖
        if (resourceIdent) {
            const factory = this.dependencyFactories.get(dep.constructor);
            if (factory === undefined) {
                throw new Error(
                    `No module factory available for dependency type: ${dep.constructor.name}`
                );
            }
            let innerMap = dependencies.get(factory);
            if (innerMap === undefined) {
                dependencies.set(factory, (innerMap = new Map()));
            }
            let list = innerMap.get(resourceIdent);
            if (list === undefined) innerMap.set(resourceIdent, (list = []));
            list.push(dep);
        }
    };

    const addDependenciesBlock = block => {
        if (block.dependencies) {
            iterationOfArrayCallback(block.dependencies, addDependency);
        }
        if (block.blocks) {
            iterationOfArrayCallback(block.blocks, addDependenciesBlock);
        }
        if (block.variables) {
            iterationBlockVariable(block.variables, addDependency);
        }
    };

    try {
        addDependenciesBlock(module);
    } catch (e) {
        callback(e);
    }

    const sortedDependencies = [];

    for (const pair1 of dependencies) {
        for (const pair2 of pair1[1]) {
            sortedDependencies.push({
                factory: pair1[0],
                dependencies: pair2[1]
            });
        }
    }

    this.addModuleDependencies(
        module,
        sortedDependencies,
        this.bail,
        null,
        true,
        callback
    );
}

接下来进入this.addModuleDependencies,在该函数中,递归进行之前的resolve=》buildMoudule过程直到所有的依赖处理完成,到此build过程就完成了。

详情参阅https://juejin.im/post/5cc51b...

4.3 compilation.seal

在上一步build完成后,build好的module保存在compilation._modules对象中,接下来需要根据这些modules生成chunks,并生成最后打包好的代码保存到compilation.assets中。

去除优化的钩子和一些支线剧情,seal方法可以简写如下:

seal(callback) {
        this.hooks.seal.call();
  
  // 初始化chunk、chunkGroups等
        for (const preparedEntrypoint of this._preparedEntrypoints) {
            const module = preparedEntrypoint.module;
            const name = preparedEntrypoint.name;
            const chunk = this.addChunk(name);
            const entrypoint = new Entrypoint(name);
            entrypoint.setRuntimeChunk(chunk);
            entrypoint.addOrigin(null, name, preparedEntrypoint.request);
            this.namedChunkGroups.set(name, entrypoint);
            this.entrypoints.set(name, entrypoint);
            this.chunkGroups.push(entrypoint);
    //在chunkGroups的chunk数组中推入chunk,在chunk的_groups Set中加入chunhGroups,建立两者联系
            GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
  //在module的_chunks Set中加入chunk,chunk的_modules Set中加入module
            GraphHelpers.connectChunkAndModule(chunk, module);

            chunk.entryModule = module;
            chunk.name = name;
      //给各个依赖的module按照引用层级加上depth属性,如入口为的depth为0
            this.assignDepth(module);
        }
  //生成module graph 和chunk graph
        buildChunkGraph(
            this,
            /** @type {Entrypoint[]} */ (this.chunkGroups.slice())
        );
        this.sortModules(this.modules);
  

        this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
            this.hooks.beforeModuleIds.call(this.modules);
            this.hooks.moduleIds.call(this.modules);
            this.applyModuleIds();
        
            this.hooks.beforeChunkIds.call(this.chunks);
            this.applyChunkIds();
            
      
            this.hooks.beforeHash.call();
            this.createHash();
            this.hooks.afterHash.call();

            return this.hooks.afterSeal.callAsync(callback);
        });
    }

首先是在compilation对象上初始化chunk、chunkGroups等变量,利用GraphHelpers方法建立module和chunk,chunk和chunkGroups之间的关系,调用assignDepth方法给每个module加上依赖层级depth,接着进入buildChunkGraph生成chunk graph。

const buildChunkGraph = (compilation, inputChunkGroups) => {
    // SHARED STATE

    /** @type {Map<ChunkGroup, ChunkGroupDep[]>} */
    const chunkDependencies = new Map();

    /** @type {Set<ChunkGroup>} */
    const allCreatedChunkGroups = new Set();

    /** @type {Map<ChunkGroup, ChunkGroupInfo>} */
    const chunkGroupInfoMap = new Map();

    /** @type {Set<DependenciesBlock>} */
    const blocksWithNestedBlocks = new Set();

    // PART ONE

    visitModules(
        compilation,
        inputChunkGroups,
        chunkGroupInfoMap,
        chunkDependencies,
        blocksWithNestedBlocks,
        allCreatedChunkGroups
    );

    // PART TWO

    connectChunkGroups(
        blocksWithNestedBlocks,
        chunkDependencies,
        chunkGroupInfoMap
    );

    // Cleaup work

    cleanupUnconnectedGroups(compilation, allCreatedChunkGroups);
};

主要逻辑在visitModules方法中,首先通过const blockInfoMap = extraceBlockInfoMap(compilation)得到module graph,module是一个Map,键名是各个module,键值是module的依赖,分为异步加载的依赖blocks和同步依赖modules:

0: {NormalModule => Object}
    key: NormalModule {dependencies: Array(4), blocks: Array(1), variables: Array(0), type: "javascript/auto", context: "/Users/hahaha/project/demo/webpack-demo/src", …}
    value:
      blocks: [ImportDependenciesBlock]
      modules: Set(1) {NormalModule}
1: {ImportDependenciesBlock => Object}
2: {NormalModule => Object}
3: {NormalModule => Object}
4: {NormalModule => Object}
5: {NormalModule => Object}
6: {NormalModule => Object}

然后利用两层循环将栈内的模块及其依赖一层层的加入到chunk的this._modules对象中,同步依赖放在内层循环处理,异步依赖放在外层循环处理。(利用栈处理递归依赖以及利用swtich进行流程管理)

接下来connectChunkGroupscleanupUnconnectedGroups,遍历 chunk graph,通过和依赖的 module 之间的使用关系来建立起不同 chunkGroup 之间的父子关系,同时剔除一些没有建立起联系的 chunk,没细看

详情:webpack系列之六chunk图生成

接下来就是生成module id和chunk id了,之前好像是生成的数字id,现在好像在NamedModulesPlugin和NamedChunksPlugin插件中将id命名成文件名了。

his.createHash方法中生成hash,包括本次编译的hash、chunkhash、modulehash。hash的生成步骤基本如下,首先create得到moduleHash方法,再在updateHash方法中不断的加各种内容,例如modulehash生成过程中就用到了module id、各种依赖、export信息等,最后调用digest方法生成hash:

const moduleHash = createHash(hashFunction);
            module.updateHash(moduleHash);
            module.hash = /** @type {string} */ (moduleHash.digest(hashDigest));
            module.renderedHash = module.hash.substr(0, hashDigestLength);

chunkhash生成过程中会用到chunk id、module id、name、template信息等。

最后就是调用

createChunkAssets() {
        const outputOptions = this.outputOptions;
        const cachedSourceMap = new Map();
        const alreadyWrittenFiles = new Map();
  //遍历chunks数组
        for (let i = 0; i < this.chunks.length; i++) {
            const chunk = this.chunks[i];
            chunk.files = [];
            let source;
            let file;
            let filenameTemplate;
            try {
        //入口模块就是hasRuntime,相对于普通模块,加了一层webpack runtime bootstrap 自执行函数包裹
                const template = chunk.hasRuntime()
                    ? this.mainTemplate
                    : this.chunkTemplate;
        //在该函数内会触发相应template.hooks.renderManifest钩子,在webpackoptionapply中注册的javaScriptModulesPlugin(一般是这个)中执行逻辑,在返回结果中推入render方法。
                const manifest = template.getRenderManifest({
                    chunk,
                    hash: this.hash,
                    fullHash: this.fullHash,
                    outputOptions,
                    moduleTemplates: this.moduleTemplates,
                    dependencyTemplates: this.dependencyTemplates
                }); // [{ render(), filenameTemplate, pathOptions, identifier, hash }]
                for (const fileManifest of manifest) {
          //缓存处理
          ........
          //调用上一步得到的render方法
                        source = fileManifest.render();    
                    }
                    this.assets[file] = source;
                    chunk.files.push(file);
                    this.hooks.chunkAsset.call(chunk, file);
                    alreadyWrittenFiles.set(file, {
                        hash: usedHash,
                        source,
                        chunk
                    });
                }
            }
        }
    }
  

当为chunkTemplate时,javaScriptModulesPlugin中的render方法:

  renderJavascript(chunkTemplate, chunk, moduleTemplate, dependencyTemplates) {
    //获取每个 chunk 当中所依赖的所有 module 最终需要渲染的代码
        const moduleSources = Template.renderChunkModules(
            chunk,
            m => typeof m.source === "function",
            moduleTemplate,
            dependencyTemplates
        );
    //最终生成 chunk 代码前对 chunk 最修改
        const core = chunkTemplate.hooks.modules.call(
            moduleSources,
            chunk,
            moduleTemplate,
            dependencyTemplates
        );
    //外层添加包裹函数
        let source = chunkTemplate.hooks.render.call(
            core,
            chunk,
            moduleTemplate,
            dependencyTemplates
        );
        if (chunk.hasEntryModule()) {
            source = chunkTemplate.hooks.renderWithEntry.call(source, chunk);
        }
        chunk.rendered = true;
        return new ConcatSource(source, ";");
    }

moduleSources示例:

{
,
/***/ "./src/foo.js":
,/*!********************!*\
,  !*** ./src/foo.js ***!
,  \********************/
,/*! exports provided: default */
,/***/ (function(module, __webpack_exports__, __webpack_require__) {

,"use strict";
,"eval("__webpack_require__.r(__webpack_exports__);\n
/* harmony default export */
__webpack_exports__[\"default\"] = 
                     (function(){\n  
                     console.log('here are foo')\n
                         }
                     );
\n\n\n//# sourceURL=webpack:///./src/foo.js?");",
/***/ 
})

最后得到source的示例:

"(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],,{
,
/***/ "./src/foo.js":
,/*!********************!*\
,  !*** ./src/foo.js ***!
,  \********************/
,/*! exports provided: default */
,/***/ (function(module, __webpack_exports__, __webpack_require__) {

,"use strict";
,"eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (function(){\n    console.log('here are foo')\n});\n\n\n//# sourceURL=webpack:///./src/foo.js?");",

/***/ }),

},])"

当为mainTemplate时,调用的是mainTemplate中的render方法如下:

render(hash, chunk, moduleTemplate, dependencyTemplates) {
        const buf = this.renderBootstrap(
            hash,
            chunk,
            moduleTemplate,
            dependencyTemplates
        );
        let source = this.hooks.render.call(
            new OriginalSource(
                Template.prefix(buf, " \t") + "\n",
                "webpack/bootstrap"
            ),
            chunk,
            hash,
            moduleTemplate,
            dependencyTemplates
        );
        if (chunk.hasEntryModule()) {
            source = this.hooks.renderWithEntry.call(source, chunk, hash);
        }
        if (!source) {
            throw new Error(
                "Compiler error: MainTemplate plugin 'render' should return something"
            );
        }
        chunk.rendered = true;
        return new ConcatSource(source, ";");
    }

得到的Bootstrap如下:

// install a JSONP callback for chunk loading
function webpackJsonpCallback(data) {
    var chunkIds = data[0];
    var moreModules = data[1];
    // add "moreModules" to the modules object,
    // then flag all "chunkIds" as loaded and fire callback
    var moduleId, chunkId, i = 0, resolves = [];
    for(;i < chunkIds.length; i++) {
        chunkId = chunkIds[i];
        if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
            resolves.push(installedChunks[chunkId][0]);
        }
        installedChunks[chunkId] = 0;
    }
    for(moduleId in moreModules) {
        if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
            modules[moduleId] = moreModules[moduleId];
        }
    }
    if(parentJsonpFunction) parentJsonpFunction(data);

    while(resolves.length) {
        resolves.shift()();
    }

};
,
// The module cache
var installedModules = {};

// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
var installedChunks = {
    "main": 0
};



// script path function
function jsonpScriptSrc(chunkId) {
    return __webpack_require__.p + "" + chunkId + ".bundle." + "eecd41ca7ca8f56e3293" + ".js"
},,// The require function,function __webpack_require__(moduleId) {,
    // Check if module is in cache
    if(installedModules[moduleId]) {
        return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
        i: moduleId,
        l: false,
        exports: {}
    };

    // Execute the module function
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // Flag the module as loaded
    module.l = true;

    // Return the exports of the module
    return module.exports;,},,// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];


    // JSONP chunk loading for javascript

    var installedChunkData = installedChunks[chunkId];
    if(installedChunkData !== 0) { // 0 means "already installed".

        // a Promise means "currently loading".
        if(installedChunkData) {
            promises.push(installedChunkData[2]);
        } else {
            // setup Promise in chunk cache
            var promise = new Promise(function(resolve, reject) {
                installedChunkData = installedChunks[chunkId] = [resolve, reject];
            });
            promises.push(installedChunkData[2] = promise);

            // start chunk loading
            var script = document.createElement('script');
            var onScriptComplete;

            script.charset = 'utf-8';
            script.timeout = 120;
            if (__webpack_require__.nc) {
                script.setAttribute("nonce", __webpack_require__.nc);
            }
            script.src = jsonpScriptSrc(chunkId);

            // create error before stack unwound to get useful stacktrace later
            var error = new Error();
            onScriptComplete = function (event) {
                // avoid mem leaks in IE.
                script.onerror = script.onload = null;
                clearTimeout(timeout);
                var chunk = installedChunks[chunkId];
                if(chunk !== 0) {
                    if(chunk) {
                        var errorType = event && (event.type === 'load' ? 'missing' : event.type);
                        var realSrc = event && event.target && event.target.src;
                        error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
                        error.name = 'ChunkLoadError';
                        error.type = errorType;
                        error.request = realSrc;
                        chunk[1](error);
                    }
                    installedChunks[chunkId] = undefined;
                }
            };
            var timeout = setTimeout(function(){
                onScriptComplete({ type: 'timeout', target: script });
            }, 120000);
            script.onerror = script.onload = onScriptComplete;
            document.head.appendChild(script);
        }
    }
    return Promise.all(promises);
};

// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;

// expose the module cache
__webpack_require__.c = installedModules;

// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
        Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
};

// define __esModule on exports
__webpack_require__.r = function(exports) {
    if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true });
};

// create a fake namespace object
// mode & 1: value is a module id, require it
// mode & 2: merge all properties of value into the ns
// mode & 4: return value when already ns object
// mode & 8|1: behave like require
__webpack_require__.t = function(value, mode) {
    if(mode & 1) value = __webpack_require__(value);
    if(mode & 8) return value;
    if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
    var ns = Object.create(null);
    __webpack_require__.r(ns);
    Object.defineProperty(ns, 'default', { enumerable: true, value: value });
    if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
    return ns;
};

// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
    var getter = module && module.__esModule ?
        function getDefault() { return module['default']; } :
        function getModuleExports() { return module; };
    __webpack_require__.d(getter, 'a', getter);
    return getter;
};

// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

// __webpack_public_path__
__webpack_require__.p = "";

// on error function for async loading
__webpack_require__.oe = function(err) { console.error(err); throw err; };,,var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;

,// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./src/index.js");"

得到的source如下:

"/******/ (function(modules) { // webpackBootstrap
,[object Object],/******/ })
,/************************************************************************/
,/******/ (,{
,
/***/ "./node_modules/css-loader/dist/cjs.js!./src/style.css":
,/*!*************************************************************!*\
,  !*** ./node_modules/css-loader/dist/cjs.js!./src/style.css ***!
,  \*************************************************************/
,/*! no static exports found */
,/***/ (function(module, exports, __webpack_require__) {

,[object Object],

/***/ }),,
,
/***/ "./node_modules/css-loader/dist/runtime/api.js":
,/*!*****************************************************!*\
,  !*** ./node_modules/css-loader/dist/runtime/api.js ***!
,  \*****************************************************/
,/*! no static exports found */
,/***/ (function(module, exports, __webpack_require__) {

,"use strict";
,[object Object],

/***/ }),,
,
/***/ "./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js":
,/*!****************************************************************************!*\
,  !*** ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js ***!
,  \****************************************************************************/
,/*! no static exports found */
,/***/ (function(module, exports, __webpack_require__) {

,"use strict";
,[object Object],

/***/ }),,
,
/***/ "./src/index.js":
,/*!**********************!*\
,  !*** ./src/index.js ***!
,  \**********************/
,/*! no exports provided */
,/***/ (function(module, __webpack_exports__, __webpack_require__) {

,"use strict";
,[object Object],

/***/ }),,
,
/***/ "./src/style.css":
,/*!***********************!*\
,  !*** ./src/style.css ***!
,  \***********************/
,/*! no static exports found */
,/***/ (function(module, exports, __webpack_require__) {

,[object Object],

/***/ }),

/******/ },)

5. compiler.emitAssets

经历了上面所有的阶段之后,所有的最终代码信息已经保存在了 Compilation 的 assets 中,当 assets 资源相关的优化工作结束后,seal 阶段也就结束了。这时候执行 seal 函数接受到 callback,callback回溯到compiler.run中,执行compiler.emitAssets.

在这个方法当中首先触发 hooks.emit 钩子函数,即将进行写文件的流程。接下来开始创建目标输出文件夹,并执行 emitFiles 方法,将内存当中保存的 assets 资源输出到目标文件夹当中,这样就完成了内存中保存的 chunk 代码写入至最终的文件

参考资料:

https://juejin.im/post/5d4d08...

查看原文

赞 2 收藏 1 评论 0

karl 发布了文章 · 2019-08-15

Webpack源码阅读之Tapable

Webpack源码阅读之Tapable

webpack采用Tapable来进行流程控制,在这套体系上,内部近百个插件有条不紊,还能支持外部开发自定义插件来扩展功能,所以在阅读webpack源码前先了解Tapable的机制是很有必要的。

Tapable的基本使用方法就不介绍了,可以参考官方文档

1. 例子

从网上拷贝了一个简单的使用例子:

//main.js
const { SyncHook } = require('tapable')

//创建一个简单的同步串行钩子
let h1 = new SyncHook(['arg1,arg2']);

//在钩子上添加订阅者,钩子被call时会触发订阅的回调函数
h1.tap('A',function(arg){
  console.log('A',arg);
  return 'b'
})
h1.tap('B',function(){
  console.log('b')
})
h1.tap('C',function(){
  console.log('c')
})

//在钩子上添加拦截器
h1.intercept({
  //钩子被call的时候触发
  call: (...args)=>{
     console.log(...args, '-------------intercept call');
  },
  //定义拦截器的时候注册taps
  register:(tap)=>{
     console.log(tap, '------------------intercept register');
  },
  //循环方法
  loop:(...args)=>{
     console.log(...args, '---------------intercept loop')
  },
  //tap调用前触发
  tap:(tap)=>{
     console.log(tap, '---------------intercept tap')
  }
})

//触发钩子
h1.call(6)

2. 调试方法

最直接的方式是在 chrome 中通过断点在关键代码上进行调试,在如何使用 Chrome 调试webpack源码中学到了调试的技巧:

我们可以用 node-inspector 在chrome中调试nodejs代码,这比命令行中调试方便太多了。nodejs 从 v6.x 开始已经内置了一个 inspector,当我们启动的时候可以加上 --inspect 参数即可:

node --inspect app.js

然后打开chrome,打开一个新页面,地址是: chrome://inspect,就可以在 chrome 中调试你的代码了。

如果你的JS代码是执行一遍就结束了,可能没时间加断点,那么你可能希望在启动的时候自动在第一行自动加上断点,可以使用这个参数 --inspect-brk,这样会自动断点在你的第一行代码上。

3. 源码分析

安装好Tapable包,根据上述方法,我们运行如下命令:

node --inspect-brk main.js 

图片描述

3.1 初始化

在构造函数处打上断点,step into可以看到SyncHook继承自Hook,上面定义了一个compile函数。

class SyncHook extends Hook {
    tapAsync() {
        throw new Error("tapAsync is not supported on a SyncHook");
    }

    tapPromise() {
        throw new Error("tapPromise is not supported on a SyncHook");
    }

    compile(options) {
        factory.setup(this, options);
        return factory.create(options);
    }
}

再step into来到Hook.js

class Hook {
    //初始化
    constructor(args) {
      if (!Array.isArray(args)) args = [];
      this._args = args;
      //订阅者数组
      this.taps = [];
      //拦截器数组
      this.interceptors = [];
      //原型上触发钩子的方法,为什么复制到构造函数上?
      this.call = this._call;
      this.promise = this._promise;
      this.callAsync = this._callAsync;
      //用于保存订阅者回调函数数组
      this._x = undefined;
    }
    ...
    }

h1初始化完成:

h1:{
  call: ƒ lazyCompileHook(...args)
  callAsync: ƒ lazyCompileHook(...args)
  interceptors: []
  promise: ƒ lazyCompileHook(...args)
  taps: []
  _args: ["options"]
  _x: undefined
}
3.2 注册观察者

Tapable采用观察者模式来进行流程管理,在钩子上使用tap方法注册观察者,钩子被call时,观察者对象上定义的回调函数按照不同规则触发(钩子类型不同,触发顺序不同)。

Step into tap方法:

//options='A', fn=f(arg)
tap(options, fn) {
        //类型检测
        if (typeof options === "string") options = { name: options };
        if (typeof options !== "object" || options === null)
            throw new Error(
                "Invalid arguments to tap(options: Object, fn: function)"
            );
        //options ==>{type: "sync", fn: fn,name:options}
        options = Object.assign({ type: "sync", fn: fn }, options);
        if (typeof options.name !== "string" || options.name === "")
            throw new Error("Missing name for tap");
      //这里调用拦截器上的register方法,当intercept定义在tap前时,会在这里调用intercept.register(options), 当intercept定义在tap后时,会在intercept方法中调用intercept.register(this.taps)
        options = this._runRegisterInterceptors(options);
        //根据before, stage 的值来排序this.taps = [{type: "sync", fn: fn,name:options}]
        this._insert(options);
    }

当三个观察者注册完成后,h1变为:

{
  call: ƒ lazyCompileHook(...args)
  callAsync: ƒ lazyCompileHook(...args)
  interceptors: []
  promise: ƒ lazyCompileHook(...args)
  taps:[
       0: {type: "sync", fn: ƒ, name: "A"}
    1: {type: "sync", fn: ƒ, name: "B"}
    2: {type: "sync", fn: ƒ, name: "C"}
  ]
  length: 3
  __proto__: Array(0)
  _args: ["options"]
_x: undefined
}
3.3 注册拦截器

在调用h1.intercept() 处step into,可以看到定义的拦截回调被推入this.interceptors中。

intercept(interceptor) {
        this._resetCompilation();
        this.interceptors.push(Object.assign({}, interceptor));
        if (interceptor.register) {
            for (let i = 0; i < this.taps.length; i++)
                this.taps[i] = interceptor.register(this.taps[i]);
        }
    }

此时h1变为:

{
  call: ƒ lazyCompileHook(...args)
  callAsync: ƒ lazyCompileHook(...args)
  interceptors: Array(1)
    0:
    call: (...args) => {…}
    loop: (...args) => {…}
    register: (tap) => {…}
    tap: (tap) => {…}
    __proto__: Object
    length: 1
    __proto__: Array(0)
  promise: ƒ lazyCompileHook(...args)
  taps: Array(3)
    0: {type: "sync", fn: ƒ, name: "A"}
    1: {type: "sync", fn: ƒ, name: "B"}
    2: {type: "sync", fn: ƒ, name: "C"}
    length: 3
    __proto__: Array(0)
  _args: ["options"]
  _x: undefined
}
3.4 钩子调用

在观察者和拦截器都注册后,会保存在this.interceptorsthis.taps中;当我们调用h1.call()函数后,会按照一定的顺序调用它们,现在我们来看看具体的流程,在call方法调用时step into, 会来到Hook.js中的createCompileDelegate函数。

function createCompileDelegate(name, type) {
    return function lazyCompileHook(...args) {
        this[name] = this._createCall(type);
        return this[name](...args);
    };
}

因为_call函数定义在Hook原型上,并通过在构造函数中this.call=this.__call赋值。

Object.defineProperties(Hook.prototype, {
    _call: {
        value: createCompileDelegate("call", "sync"),
        configurable: true,
        writable: true
    },
    _promise: {
        value: createCompileDelegate("promise", "promise"),
        configurable: true,
        writable: true
    },
    _callAsync: {
        value: createCompileDelegate("callAsync", "async"),
        configurable: true,
        writable: true
    }
});

按照执行顺序转到 this._createCall

_createCall(type) {
        return this.compile({
            taps: this.taps,
            interceptors: this.interceptors,
            args: this._args,
            type: type
        });
    }

this.compile()处step into 跳转到SyncHook.js上的compile方法上,其实我们在Hook.js上就可以看到,compile是需要在子类上重写的方法, 在SyncHook上其实现如下:

compile(options) {
        factory.setup(this, options);
        return factory.create(options);
    }

class SyncHookCodeFactory extends HookCodeFactory {
    content({ onError, onDone, rethrowIfPossible }) {
        return this.callTapsSeries({
            onError: (i, err) => onError(err),
            onDone,
            rethrowIfPossible
        });
    }
}

const factory = new SyncHookCodeFactory();

factory.setup处step into,可以看到factory.setup(this, options)其实只是把taps上注册的回调推入this._x:

    setup(instance, options) {
        instance._x = options.taps.map(t => t.fn);
    }

factory.create中定义了this.interceptorsthis.taps的具体执行顺序,在这里step into:

//HookFactory.js
create(options) {
        this.init(options);
        let fn;
        switch (this.options.type) {
            case "sync":
                fn = new Function(
                    this.args(),
                    '"use strict";\n' +
                        this.header() +
                        this.content({
                            onError: err => `throw ${err};\n`,
                            onResult: result => `return ${result};\n`,
                            resultReturns: true,
                            onDone: () => "",
                            rethrowIfPossible: true
                        })
                );
                break;
            case "async":
                ....
            case "promise":
                ....
        }
        this.deinit();
        return fn;
    }

可以看到这里是通过new Function构造函数传入this.interceptorsthis.taps动态进行字符串拼接生成函数体执行的。

this.header()中打断点:

header() {
        let code = "";
        if (this.needContext()) {
            code += "var _context = {};\n";
        } else {
            code += "var _context;\n";
        }
        code += "var _x = this._x;\n";
        if (this.options.interceptors.length > 0) {
            code += "var _taps = this.taps;\n";
            code += "var _interceptors = this.interceptors;\n";
        }
        for (let i = 0; i < this.options.interceptors.length; i++) {
            const interceptor = this.options.interceptors[i];
            if (interceptor.call) {
                code += `${this.getInterceptor(i)}.call(${this.args({
                    before: interceptor.context ? "_context" : undefined
                })});\n`;
            }
        }
        return code;
    }

生成的code如下,其执行了拦截器中定义的call回调:

"var _context;
var _x = this._x;
var _taps = this.taps;
var _interceptors = this.interceptors;
_interceptors[0].call(options);

this.content()打断点,可以看到this.content定义在HookCodeFactory中:

class SyncHookCodeFactory extends HookCodeFactory {
    content({ onError, onDone, rethrowIfPossible }) {
        return this.callTapsSeries({
            onError: (i, err) => onError(err),
            onDone,
            rethrowIfPossible
        });
    }
}

其返回了定义在子类中的callTapsSeries方法:

callTapsSeries({
        onError,
        onResult,
        resultReturns,
        onDone,
        doneReturns,
        rethrowIfPossible
    }) {
        if (this.options.taps.length === 0) return onDone();
        const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");
        const somethingReturns = resultReturns || doneReturns || false;
        let code = "";
        let current = onDone;
        for (let j = this.options.taps.length - 1; j >= 0; j--) {
            const i = j;
            const unroll = current !== onDone && this.options.taps[i].type !== "sync";
            if (unroll) {
                code += `function _next${i}() {\n`;
                code += current();
                code += `}\n`;
                current = () => `${somethingReturns ? "return " : ""}_next${i}();\n`;
            }
            const done = current;
            const doneBreak = skipDone => {
                if (skipDone) return "";
                return onDone();
            };
            const content = this.callTap(i, {
                onError: error => onError(i, error, done, doneBreak),
                onResult:
                    onResult &&
                    (result => {
                        return onResult(i, result, done, doneBreak);
                    }),
                onDone: !onResult && done,
                rethrowIfPossible:
                    rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
            });
            current = () => content;
        }
        code += current();
        return code;
    }

具体的拼接步骤这里就不详述了,感兴趣可以自己debugger,嘿嘿。最后返回的code为:

var _tap0 = _taps[0];
_interceptors[0].tap(_tap0);
var _fn0 = _x[0];
_fn0(options);
var _tap1 = _taps[1];
_interceptors[0].tap(_tap1);
var _fn1 = _x[1];
_fn1(options);
var _tap2 = _taps[2];
_interceptors[0].tap(_tap2);
var _fn2 = _x[2];
_fn2(options);
var _tap3 = _taps[3];
_interceptors[0].tap(_tap3);
var _fn3 = _x[3];
_fn3(options);

这里定义了taps和其相应的拦截器的执行顺序。

4. webpack调试技巧

当我们调试webpack源码是,经常需要在钩子被call的代码处调试到具体插件的执行过程,可以参考上述过程进行调试,具体步骤为:

  • 在call处step into

图片描述

  • 在return处step into

图片描述

  • 得到生成的动态函数

    (function anonymous(options
    ) {
    "use strict";
      var _context;
      var _x = this._x;
      var _taps = this.taps;
      var _interceptors = this.interceptors;
      _interceptors[0].call(options);
      var _tap0 = _taps[0];
      _interceptors[0].tap(_tap0);
      var _fn0 = _x[0];
      _fn0(options);
      var _tap1 = _taps[1];
      _interceptors[0].tap(_tap1);
      var _fn1 = _x[1];
      _fn1(options);
      var _tap2 = _taps[2];
      _interceptors[0].tap(_tap2);
      var _fn2 = _x[2];
      _fn2(options);
      var _tap3 = _taps[3];
      _interceptors[0].tap(_tap3);
      var _fn3 = _x[3];
      _fn3(options);
    })
  • 在fn(options)处打step into

    图片描述

  • 回到tap注册的函数

    h1.tap('A', function (arg) {
        console.log('A',arg);
        return 'b'; 
    })
查看原文

赞 1 收藏 0 评论 0

karl 赞了文章 · 2019-08-08

webpack模块化原理-ES module

上一篇文章介绍了webpack对commonjs模块的支持(如果你还没读过,建议你先阅读),这篇文章来探究一下,webpack是如何支持es模块的。

准备

我们依然写两个文件,m.js文件用es模块的方式export一个default函数和一个foo函数,index.js import该模块,具体代码如下:

// m.js
'use strict';
export default function bar () {
    return 1;
};
export function foo () {
    return 2;
}
// index.js
'use strict';
import bar, {foo} from './m';
bar();
foo();

webpack配置没有变化,依然以index.js作为入口:

var path = require("path");
module.exports = {
    entry: path.join(__dirname, 'index.js'),
    output: {
        path: path.join(__dirname, 'outs'),
        filename: 'index.js'
    },
};

在根目录下执行webpack,得到经过webpack打包的代码如下(去掉了不必要的注释):

(function(modules) { // webpackBootstrap
    // The module cache
    var installedModules = {};
    // The require function
    function __webpack_require__(moduleId) {
        // Check if module is in cache
        if(installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }
        // Create a new module (and put it into the cache)
        var module = installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {}
        };
        // Execute the module function
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        // Flag the module as loaded
        module.l = true;
        // Return the exports of the module
        return module.exports;
    }
    // expose the modules object (__webpack_modules__)
    __webpack_require__.m = modules;
    // expose the module cache
    __webpack_require__.c = installedModules;
    // define getter function for harmony exports
    __webpack_require__.d = function(exports, name, getter) {
        if(!__webpack_require__.o(exports, name)) {
            Object.defineProperty(exports, name, {
                configurable: false,
                enumerable: true,
                get: getter
            });
        }
    };
    // getDefaultExport function for compatibility with non-harmony modules
    __webpack_require__.n = function(module) {
        var getter = module && module.__esModule ?
            function getDefault() { return module['default']; } :
            function getModuleExports() { return module; };
        __webpack_require__.d(getter, 'a', getter);
        return getter;
    };
    // Object.prototype.hasOwnProperty.call
    __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
    // __webpack_public_path__
    __webpack_require__.p = "";
    // Load entry module and return exports
    return __webpack_require__(__webpack_require__.s = 0);
})
([
(function(module, __webpack_exports__, __webpack_require__) {

    "use strict";
    Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
    /* harmony import */
    var __WEBPACK_IMPORTED_MODULE_0__m__ = __webpack_require__(1);

    Object(__WEBPACK_IMPORTED_MODULE_0__m__["a" /* default */])();
    Object(__WEBPACK_IMPORTED_MODULE_0__m__["b" /* foo */])();

}),
(function(module, __webpack_exports__, __webpack_require__) {

    "use strict";
    /* harmony export (immutable) */
    __webpack_exports__["a"] = bar;
    /* harmony export (immutable) */
    __webpack_exports__["b"] = foo;

    function bar () {
        return 1;
    };
    function foo () {
        return 2;
    }

})
]);

分析

上一篇文章已经分析过了,webpack生成的代码是一个IIFE,这个IIFE完成一系列初始化工作后,就会通过__webpack_require__(0)启动入口模块。

我们首先来看m.js模块是如何实现es的export的,被webpack转换后的m.js代码如下:

__webpack_exports__["a"] = bar;
__webpack_exports__["b"] = foo;

function bar () {
    return 1;
};
function foo () {
    return 2;
}

其实一眼就能看出来,export default和export都被转换成了类似于commonjs的exports.xxx,这里也已经不区分是不是default export了,所有的export对象都是__webpack_exports__的属性。

我们继续来看看入口模块,被webpack转换后的index.js代码如下:

Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
var __WEBPACK_IMPORTED_MODULE_0__module__ = __webpack_require__(1);

Object(__WEBPACK_IMPORTED_MODULE_0__m__["a" /* default */])();
Object(__WEBPACK_IMPORTED_MODULE_0__m__["b" /* foo */])();

index模块首先通过Object.defineProperty__webpack_exports__上添加属性__esModule ,值为true,表明这是一个es模块。在目前的代码下,这个标记是没有作用的,至于在什么情况下需要判断模块是否es模块,后面会分析。

然后就是通过__webpack_require__(1)导入m.js模块,再然后通过module.xxx获取m.js中export的对应属性。注意这里有一个重要的点,就是所有引入的模块属性都会用Object()包装成对象,这是为了保证像Boolean、String、Number这些基本数据类型转换成相应的类型对象。

commonjs与es6 module混用

我们前面分析的都是commonjs模块对commonjs模块的导入,或者es模块对es模块的导入,那么如果是es模块对commonjs模块的导入会是什么情况呢,反过来又会如何呢?

其实我们前面说到的__webpack_exports__. __esModule = true就是针对这种情况的解决方法。

下面用具体代码来解释一下,首先修改m.js和index.js代码如下:

// m.js
'use strict';
exports.foo = function () {
    return 1;
}
// index.js
'use strict';
import m from './m';
m.foo();

重新执行webpack后生成的代码如下(只截取IIFE的参数部分):

[
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {

    "use strict";
    Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
    /* harmony import */ 
    var __WEBPACK_IMPORTED_MODULE_0__m__ = __webpack_require__(1);
    /* harmony import */ 
    var __WEBPACK_IMPORTED_MODULE_0__m___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__m__);

    __WEBPACK_IMPORTED_MODULE_0__m___default.a.foo();

}),
/* 1 */
(function(module, exports, __webpack_require__) {

    "use strict";
    exports.foo = function () {
        return 1;
    }

})
]

m.js转换后的代码跟转换前的代码基本没有变化,都是用webpack提供的exports进行模块导出。但是index.js有一点不同,主要是多了一行代码:

var __WEBPACK_IMPORTED_MODULE_0__m___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__m__);

这段代码作用是什么呢,看一下__webpack_require__.n的定义就知道了:

// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
    var getter = module && module.__esModule ?
        function getDefault() { return module['default']; } :
        function getModuleExports() { return module; };
    __webpack_require__.d(getter, 'a', getter);
    return getter;
};

__webpack_require__.n会判断module是否为es模块,当__esModule为true的时候,标识module为es模块,那么module.a默认返回module.default,否则返回module

具体实现则是通过 __webpack_require__.d将具体操作绑定到属性a的getter方法上的。

那么,当通过es模块的方式去import一个commonjs规范的模块时,就会把require得到的module进行一层包装,从而兼容两种情况。

至于通过commonjs去require一个es模块的情况,原理相同,就不过多解释了。

结论

webpack对于es模块的实现,也是基于自己实现的__webpack_require__ __webpack_exports__ ,装换成类似于commonjs的形式。对于es模块和commonjs混用的情况,则需要通过__webpack_require__.n的形式做一层包装来实现。

下一篇webpack模块化原理-Code Splitting,会继续来分析webpack是如何通过动态importmodule.ensure实现Code Splitting的。

查看原文

赞 80 收藏 59 评论 2

karl 赞了文章 · 2019-08-08

webpack模块化原理-ES module

上一篇文章介绍了webpack对commonjs模块的支持(如果你还没读过,建议你先阅读),这篇文章来探究一下,webpack是如何支持es模块的。

准备

我们依然写两个文件,m.js文件用es模块的方式export一个default函数和一个foo函数,index.js import该模块,具体代码如下:

// m.js
'use strict';
export default function bar () {
    return 1;
};
export function foo () {
    return 2;
}
// index.js
'use strict';
import bar, {foo} from './m';
bar();
foo();

webpack配置没有变化,依然以index.js作为入口:

var path = require("path");
module.exports = {
    entry: path.join(__dirname, 'index.js'),
    output: {
        path: path.join(__dirname, 'outs'),
        filename: 'index.js'
    },
};

在根目录下执行webpack,得到经过webpack打包的代码如下(去掉了不必要的注释):

(function(modules) { // webpackBootstrap
    // The module cache
    var installedModules = {};
    // The require function
    function __webpack_require__(moduleId) {
        // Check if module is in cache
        if(installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }
        // Create a new module (and put it into the cache)
        var module = installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {}
        };
        // Execute the module function
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        // Flag the module as loaded
        module.l = true;
        // Return the exports of the module
        return module.exports;
    }
    // expose the modules object (__webpack_modules__)
    __webpack_require__.m = modules;
    // expose the module cache
    __webpack_require__.c = installedModules;
    // define getter function for harmony exports
    __webpack_require__.d = function(exports, name, getter) {
        if(!__webpack_require__.o(exports, name)) {
            Object.defineProperty(exports, name, {
                configurable: false,
                enumerable: true,
                get: getter
            });
        }
    };
    // getDefaultExport function for compatibility with non-harmony modules
    __webpack_require__.n = function(module) {
        var getter = module && module.__esModule ?
            function getDefault() { return module['default']; } :
            function getModuleExports() { return module; };
        __webpack_require__.d(getter, 'a', getter);
        return getter;
    };
    // Object.prototype.hasOwnProperty.call
    __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
    // __webpack_public_path__
    __webpack_require__.p = "";
    // Load entry module and return exports
    return __webpack_require__(__webpack_require__.s = 0);
})
([
(function(module, __webpack_exports__, __webpack_require__) {

    "use strict";
    Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
    /* harmony import */
    var __WEBPACK_IMPORTED_MODULE_0__m__ = __webpack_require__(1);

    Object(__WEBPACK_IMPORTED_MODULE_0__m__["a" /* default */])();
    Object(__WEBPACK_IMPORTED_MODULE_0__m__["b" /* foo */])();

}),
(function(module, __webpack_exports__, __webpack_require__) {

    "use strict";
    /* harmony export (immutable) */
    __webpack_exports__["a"] = bar;
    /* harmony export (immutable) */
    __webpack_exports__["b"] = foo;

    function bar () {
        return 1;
    };
    function foo () {
        return 2;
    }

})
]);

分析

上一篇文章已经分析过了,webpack生成的代码是一个IIFE,这个IIFE完成一系列初始化工作后,就会通过__webpack_require__(0)启动入口模块。

我们首先来看m.js模块是如何实现es的export的,被webpack转换后的m.js代码如下:

__webpack_exports__["a"] = bar;
__webpack_exports__["b"] = foo;

function bar () {
    return 1;
};
function foo () {
    return 2;
}

其实一眼就能看出来,export default和export都被转换成了类似于commonjs的exports.xxx,这里也已经不区分是不是default export了,所有的export对象都是__webpack_exports__的属性。

我们继续来看看入口模块,被webpack转换后的index.js代码如下:

Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
var __WEBPACK_IMPORTED_MODULE_0__module__ = __webpack_require__(1);

Object(__WEBPACK_IMPORTED_MODULE_0__m__["a" /* default */])();
Object(__WEBPACK_IMPORTED_MODULE_0__m__["b" /* foo */])();

index模块首先通过Object.defineProperty__webpack_exports__上添加属性__esModule ,值为true,表明这是一个es模块。在目前的代码下,这个标记是没有作用的,至于在什么情况下需要判断模块是否es模块,后面会分析。

然后就是通过__webpack_require__(1)导入m.js模块,再然后通过module.xxx获取m.js中export的对应属性。注意这里有一个重要的点,就是所有引入的模块属性都会用Object()包装成对象,这是为了保证像Boolean、String、Number这些基本数据类型转换成相应的类型对象。

commonjs与es6 module混用

我们前面分析的都是commonjs模块对commonjs模块的导入,或者es模块对es模块的导入,那么如果是es模块对commonjs模块的导入会是什么情况呢,反过来又会如何呢?

其实我们前面说到的__webpack_exports__. __esModule = true就是针对这种情况的解决方法。

下面用具体代码来解释一下,首先修改m.js和index.js代码如下:

// m.js
'use strict';
exports.foo = function () {
    return 1;
}
// index.js
'use strict';
import m from './m';
m.foo();

重新执行webpack后生成的代码如下(只截取IIFE的参数部分):

[
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {

    "use strict";
    Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
    /* harmony import */ 
    var __WEBPACK_IMPORTED_MODULE_0__m__ = __webpack_require__(1);
    /* harmony import */ 
    var __WEBPACK_IMPORTED_MODULE_0__m___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__m__);

    __WEBPACK_IMPORTED_MODULE_0__m___default.a.foo();

}),
/* 1 */
(function(module, exports, __webpack_require__) {

    "use strict";
    exports.foo = function () {
        return 1;
    }

})
]

m.js转换后的代码跟转换前的代码基本没有变化,都是用webpack提供的exports进行模块导出。但是index.js有一点不同,主要是多了一行代码:

var __WEBPACK_IMPORTED_MODULE_0__m___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__m__);

这段代码作用是什么呢,看一下__webpack_require__.n的定义就知道了:

// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
    var getter = module && module.__esModule ?
        function getDefault() { return module['default']; } :
        function getModuleExports() { return module; };
    __webpack_require__.d(getter, 'a', getter);
    return getter;
};

__webpack_require__.n会判断module是否为es模块,当__esModule为true的时候,标识module为es模块,那么module.a默认返回module.default,否则返回module

具体实现则是通过 __webpack_require__.d将具体操作绑定到属性a的getter方法上的。

那么,当通过es模块的方式去import一个commonjs规范的模块时,就会把require得到的module进行一层包装,从而兼容两种情况。

至于通过commonjs去require一个es模块的情况,原理相同,就不过多解释了。

结论

webpack对于es模块的实现,也是基于自己实现的__webpack_require__ __webpack_exports__ ,装换成类似于commonjs的形式。对于es模块和commonjs混用的情况,则需要通过__webpack_require__.n的形式做一层包装来实现。

下一篇webpack模块化原理-Code Splitting,会继续来分析webpack是如何通过动态importmodule.ensure实现Code Splitting的。

查看原文

赞 80 收藏 59 评论 2

karl 发布了文章 · 2019-07-10

Babel 7使用总结

Babel 7使用总结

​ 2019-07-08

本文基于Babel 7.4.5。

图片描述
​ Babel主要模块如上图所示,接下来将分别介绍。

1. @babel/core

@babel/core主要是进行代码转换的一些方法,可以将源代码根据配置转换成兼容目标环境的代码。

import * as babel from "@babel/core";
babel.transform("code();", options, function(err, result) {
  result.code;
  result.map;
  result.ast;
});

2. @babel/cli

@babel/cli是 babel 提供的命令行工具,用于命令行下编译源代码。

首先安装依赖:

npm install --save-dev @babel/core @babel/cli

新建一个js文件:

let array = [1,2,3,4,5,6];
array.includes(function(item){
    return item>2;
})
class Robot {
    constructor (msg) {
        this.message = msg
    }
    say () {
        alertMe(this.message)
    }
}
Object.assign({},{
    a:1,b:2
})
const fn = () => 1;
new Promise();

执行转换:

npx babel index.js --out-file out.js

可以发现输出代码没有变化,这是因为没有进行配置来确定怎么进行转换。

3. @babel/plugin*

babel是通过插件来进行代码转换的,例如箭头函数使用plugin-transform-arrow-functions插件来进行转换。

首先安装该插件:

npm install --save-dev @babel/plugin-transform-arrow-functions

可以通过@babel/cli传参或者配置文件的方式使用插件:

  • @babel/cli

    npx babel index.js --out-file out.js --plugins=@babel/plugin-transform-arrow-functions

    则可以得到out.js文件,可以看到箭头函数已经被转换。

    let array = [1, 2, 3, 4, 5, 6];
    array.includes(function (item) {
      return item > 2;
    });
    class Robot {
        constructor (msg) {
            this.message = msg
        }
        say () {
            alertMe(this.message)
        }
    }
    Object.assign({}, {
      a: 1,
      b: 2
    });
    const fn = function () {
      return 1;
    };
    
    new Promise();
  • 配置文件babel.config.js(javascript写法)或.babelrc(json写法),使用配置文件是更加常用的方式。

    module.exports = function (api) {
        api.cache(true);
    
        const plugins = [ "@babel/plugin-transform-arrow-functions" ];
    
        return {
            plugins
        };
    }

4. @babel/presets

我们在index.js中使用了多种es6的语法,一个个的导入插件很麻烦,presets是一组预设好的插件集合。官方为常见环境组装了一些 presets (当然也可以自己配置):

我们使用@babel/preset-env为例(使用前需npm install @babel/preset-env):

module.exports = function (api) {
    api.cache(true);
    const presets =  [
        ["@babel/preset-env"]
    ];
    return {
        presets
    };
}

得到的结果如下, 可以看到箭头函数被编译、es6类、let声明被编译了。

"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

var array = [1, 2, 3, 4, 5, 6];
array.includes(function (item) {
  return item > 2;
});

var Robot =
/*#__PURE__*/
function () {
  function Robot(msg) {
    _classCallCheck(this, Robot);

    this.message = msg;
  }

  _createClass(Robot, [{
    key: "say",
    value: function say() {
      alertMe(this.message);
    }
  }]);

  return Robot;
}();

Object.assign({}, {
  a: 1,
  b: 2
});

var fn = function fn() {
  return 1;
};

new Promise();

但是可以看到数组的实例方法includes、对象的静态方法,以及promise并没有被编译。

这是因为babel 把 Javascript 语法为syntax 和 api, api 指那些我们可以通过 函数重新覆盖的语法 ,类似 includes, map, includes, Promise, 凡是我们能想到重写的都可以归属到api。syntax 指像箭头函数,let,const,class, 依赖注入 Decorators等等这些,我们在 Javascript在运行是无法重写的,想象下,在不支持的浏览器里不管怎么样,你都用不了 let 这个关键字。

@babel/presets默认只对syntax进行转换,我们需要使用@babel/polyfill来提供对api的的支持。

5. @babel/polyfill

@babel/polyfill由core-js2和regenerator-runtime组成,后者是facebook开源库,用来实现对generator、async函数等的支持,前者是js标准库,包含不同版本javascipt语法的实现。

只要在js文件的入口顶部引入@babel/polyfill就可以在后问的代码中自由的使用es6 api了。

但是整体@babel/polyfill整个包体积较大,我们通常只使用了其中一部分方法,而引入整个库显然是不合适的。所以你可以只引入使用的方法:

import 'core-js/features/array/from'; // <- at the top of your entry point
import 'core-js/features/array/flat'; // <- at the top of your entry point
import 'core-js/features/set';        // <- at the top of your entry point
import 'core-js/features/promise';    // <- at the top of your entry point

Array.from(new Set([1, 2, 3, 2, 1]));          // => [1, 2, 3]
[1, [2, 3], [4, [5]]].flat(2);                 // => [1, 2, 3, 4, 5]
Promise.resolve(32).then(x => console.log(x)); // => 32

如果你不想污染全局命名空间(例如在写一个npm库时,要保持其隔离性)。可以引入纯净版:

import from from 'core-js-pure/features/array/from';
import flat from 'core-js-pure/features/array/flat';
import Set from 'core-js-pure/features/set';
import Promise from 'core-js-pure/features/promise';

from(new Set([1, 2, 3, 2, 1]));                // => [1, 2, 3]
flat([1, [2, 3], [4, [5]]], 2);                // => [1, 2, 3, 4, 5]
Promise.resolve(32).then(x => console.log(x)); // => 32

preset-env的配置项中的useBuiltIns属性可以方便@babel/polyfill的使用。

  • useBuiltIns:false(default):此时不对 polyfill 做操作。如果引入 @babel/polyfill,则无视配置的浏览器兼容,引入所有的 polyfill
  • useBuiltIns:"entry":根据配置的浏览器兼容,引入浏览器不兼容的 polyfill。需要在入口文件手动添加 import '@babel/polyfill',会自动根据 browserslist 替换成浏览器不兼容的所有 polyfill
  • useBuiltIns:"usage":不需要在文件顶部手动引入@babel/polyfill,会根据代码中的使用进行按需添加。

在这里使用useBuiltIns:"usage"作为示例,babel.config.js文件如下:

module.exports = function (api) {
    api.cache(true);

    const presets =  [
        ["@babel/preset-env",
            {
            "useBuiltIns": "usage",
            "targets":{
                "browsers":["> 1%", "last 2 versions", "not ie                                                             <= 8"]
                }
            }
        ]
    ];
    return {
        presets,
        // plugins
    };
}

得到的编译结果:

"use strict";

require("core-js/modules/es6.promise");

require("core-js/modules/es6.object.to-string");

require("core-js/modules/es6.object.assign");

require("core-js/modules/es7.array.includes");

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

var array = [1, 2, 3, 4, 5, 6];
array.includes(function (item) {
  return item > 2;
});

var Robot =
/*#__PURE__*/
function () {
  function Robot(msg) {
    _classCallCheck(this, Robot);

    this.message = msg;
  }

  _createClass(Robot, [{
    key: "say",
    value: function say() {
      alertMe(this.message);
    }
  }]);

  return Robot;
}();

Object.assign({}, {
  a: 1,
  b: 2
});

var fn = function fn() {
  return 1;
};

new Promise();

可以看到实现了polyfill的按需引入。但是在配置文件中未指定core-js版本时,默认会使用core-js2。命令行会出现如下提示:

WARNING: We noticed you're using the useBuiltIns option without declaring a core-js version. Currently, we assume version 2.x when no version is passed. Since this default version will likely change in future versions of Babel, we recommend explicitly setting the core-js version you are using via the corejs option.

这是因为core-js3已经发布,@babel/polyfill不支持从core-js2到core-js3的平滑过渡,所以在babel 7.4版本中,已经废弃@babel/polyfill(只能用core-js2),而是直接引入core-js3和regenerator-runtime代替。

import "@babel/polyfill";

// migration

import "core-js/stable";
import "regenerator-runtime/runtime";

使用core-js3有很多优点,首先就是新,包含很多新特性,其次就是可以配合@babel/runtime(后文详述)。更多优点见core-js@3, babel and a look into the future

使用core-js3是 babel.config.js如下:

module.exports = function (api) {
    api.cache(true);

    const presets =  [
        ["@babel/preset-env",
            {
            "useBuiltIns": "usage",
            "corejs":3,
            "targets":{
                "browsers":["> 1%", "last 2 versions", "not ie <= 8"]
                }
            }
        ]
    ];
    return {
        presets,
        // plugins
    };
}

仔细观察上面的编译结果可以发现有两个问题。

  • 高阶语法向低阶语法转化时引入了了很多helper函数(如_classCallCheck)。当文件数量很多时,每个文件都引入这些helper函数会使得文件体积增大,怎么这些helper函数抽离到单独的模块,然后按需引入呢?
  • 虽然polyfill是按需引入的,但是会污染全局命名空间,当你写的是公共库时,可能会与使用者本地的方法产生冲突。例如你在你的库中引入了polyfill中的Promise,使用者自身定义了自己的Promise,这就容易产生冲突。如何将你的公共库中引入的polyfill api隔离起来呢?

要解决这两个问题,就要需要使用@babel/runtime和@babel/plugin-transform-runtime了。

6. @babel/runtime

@babel/runtime依赖@babel/helpers和regenerator-runtime,helper函数都可以从这里面引入,手动的肯定不可能,于是 babel 提供了 @babel/plugin-transform-runtime 来替我们做这些转换。

babel.config.js文件为:

module.exports = function (api) {
    api.cache(true);

    const presets =  [
        ["@babel/preset-env",
            {
            "useBuiltIns": "usage",
            "targets":{
                "browsers":["> 1%", "last 2 versions", "not ie <= 8"]
                }
            }
        ]
    ];
    const plugins = [
        ["@babel/plugin-transform-runtime"]
    ]

    return {
        presets,
        plugins
    };
}

得到的编译结果是:

"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
require("core-js/modules/es6.promise");
require("core-js/modules/es6.object.to-string");
require("core-js/modules/es6.object.assign");

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
require("core-js/modules/es7.array.includes");

var array = [1, 2, 3, 4, 5, 6];
array.includes(function (item) {
  return item > 2;
});
var Robot =
/*#__PURE__*/
function () {
  function Robot(msg) {
    (0, _classCallCheck2.default)(this, Robot);
    this.message = msg;
  }

  (0, _createClass2.default)(Robot, [{
    key: "say",
    value: function say() {
      alertMe(this.message);
    }
  }]);
  return Robot;
}();
Object.assign({}, {
  a: 1,
  b: 2
});
var fn = function fn() {
  return 1;
};
new Promise();

可以看到我们的第一个问题已经圆满解决了。

解决第二个问题需要使用@babel/plugin-transform-runtime option中的corejs参数。默认为false,不对polyfill进行处理。可以设为不同版本的core-js。

例如使用core-js2时,需要先安装

npm install --save @babel/runtime-corejs2

配置文件为:

module.exports = function (api) {
    api.cache(true);

    const presets =  [
        ["@babel/preset-env",
            {
            "useBuiltIns": "usage",
            "targets":{
                "browsers":["> 1%", "last 2 versions", "not ie <= 8"]
                }
            }
        ]
    ];
    const plugins = [
        ["@babel/plugin-transform-runtime",{corejs:2}]
    ]

    return {
        presets,
        plugins
    };
}

得到的结果是:

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");

var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));

var _assign = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/assign"));

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/createClass"));

require("core-js/modules/es7.array.includes");

var array = [1, 2, 3, 4, 5, 6];
array.includes(function (item) {
  return item > 2;
});

var Robot =
/*#__PURE__*/
function () {
  function Robot(msg) {
    (0, _classCallCheck2.default)(this, Robot);
    this.message = msg;
  }

  (0, _createClass2.default)(Robot, [{
    key: "say",
    value: function say() {
      alertMe(this.message);
    }
  }]);
  return Robot;
}();

(0, _assign.default)({}, {
  a: 1,
  b: 2
});

var fn = function fn() {
  return 1;
};

new _promise.default();

可以看到polyfill引入时得到了一个别名,可以避免全局变量污染,但是可以发现实例方法includes并没有得到相应的处理。这是core-js2没有解决的问题,随着2019年3月core-js3的发布,这个问题得到了完美解决。我们将corejs设为3,得到了结果如下:

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

var _assign = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/object/assign"));

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/createClass"));

var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));

var array = [1, 2, 3, 4, 5, 6];
(0, _includes.default)(array).call(array, function (item) {
  return item > 2;
});

var Robot =
/*#__PURE__*/
function () {
  function Robot(msg) {
    (0, _classCallCheck2.default)(this, Robot);
    this.message = msg;
  }

  (0, _createClass2.default)(Robot, [{
    key: "say",
    value: function say() {
      alertMe(this.message);
    }
  }]);
  return Robot;
}();

(0, _assign.default)({}, {
  a: 1,
  b: 2
});

var fn = function fn() {
  return 1;
};

new _promise.default();

7. @babel/register

经过 babel 的编译后,我们的源代码与运行在生产下的代码是不一样的。

babel-register 则提供了动态编译。换句话说,我们的源代码能够真正运行在生产环境下,不需要 babel 编译这一环节。

我们先在项目下安装 babel-register:

$ npm install --save-dev @babel/register

然后在入口文件中 require

require('@babel/register')
require('./app')

在入口文件头部引入 @babel/register 后,我们的 app 文件中即可使用任意 es2015 的特性。

当然,坏处是动态编译,导致程序在速度、性能上有所损耗。(我们在启动测试脚本的时候可以使用)

7. @babel/node

我们上面说,babel-register 提供动态编译,能够让我们的源代码真正运行在生产环境下 - 但其实不然,我们仍需要做部分调整,比如新增一个入口文件,并在该文件中 require('@babel/register')。而 babel-node 能真正做到一行源代码都不需要调整:

$ npm install --save-dev @babel/core @babel/node
$ npx babel-node app.js

只是,请不要在生产环境中使用 babel-node,因为它是动态编译源代码,应用启动速度非常慢

参考

http://babel.docschina.org/docs/en/babel-plugin-transform-runtime#technical-details

https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md

https://blog.hhking.cn/2019/0...

https://segmentfault.com/a/11...

https://zhuanlan.zhihu.com/p/...

https://blog.zfanw.com/babel-...

https://www.thebasement.be/up...

查看原文

赞 15 收藏 10 评论 7

认证与成就

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

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-03-29
个人主页被 481 人浏览