天天看点

Seajs源码解析系列(四)

前言:前三章对Seajs的基础应用,核心模块以及路径解析功能都做了介绍,这一章则对Seajs剩下的几项功能做一个综合的介绍。主要包括Seajs事件机制,脚本加载以及模块依赖。

代码解析:

一、Seajs事件机制:

Seajs内部提供了以下几种事件类型:

seajs.on seajs.on(event, callback)

用来添加事件回调。

// 给 fetch 事件添加一个回调
seajs.on('fetch', function(data) {
  ...
});
           

seajs.off seajs.off(event?, callback?)

用来移除事件回调。

// 从 fetch 事件的回调中移除掉 fn 函数
seajs.off('fetch', fn);

// 移除掉 fetch 事件的所有回调
seajs.off('fetch');

// 移除掉所有事件的所有回调
seajs.off();
           

seajs.emit seajs.emit(event, data)

用来触发事件。

// 触发 fetch 事件
seajs.emit('fetch', { uri: uri, fetchedList: fetchedList });
           

以下就是Seajs事件机制的源码,注释十分详细,这里就不多做介绍了。

/**
 * util-events.js - The minimal events support
 */

var events = data.events = {}

// Bind event
seajs.on = function(name, callback) {
  var list = events[name] || (events[name] = [])
  list.push(callback)
  return seajs
}

// Remove event. If `callback` is undefined, remove all callbacks for the
// event. If `event` and `callback` are both undefined, remove all callbacks
// for all events
seajs.off = function(name, callback) {
  // Remove *all* events
  if (!(name || callback)) {
    events = data.events = {}
    return seajs
  }

  var list = events[name]
  if (list) {
    if (callback) {
      for (var i = list.length - ; i >= ; i--) {
        if (list[i] === callback) {
          list.splice(i, )
        }
      }
    }
    else {
      delete events[name]
    }
  }

  return seajs
}

// Emit event, firing all bound callbacks. Callbacks receive the same
// arguments as `emit` does, apart from (除了) the event name
//触发事件
//事件的回调函数链保存在seajs.data.events中;
//以事件名称为key,Array对象保存的回调函数链为value
var emit = seajs.emit = function(name, data) {
  var list = events[name]

  if (list) {
    // Copy callback lists to prevent modification
    list = list.slice()

    // Execute event callbacks, use index because it's the faster.
    for(var i = , len = list.length; i < len; i++) {
      list[i](data)
    }
  }

  return seajs
}
           

二、Seajs脚本加载

seajs.request()方法用于从服务端请求对应的模块。

//从服务端请求模块
  function sendRequest() {
    seajs.request(emitData.requestUri, emitData.onRequest, emitData.charset, emitData.crossorigin)
  }
           

源码如下:当浏览器支持webworker时,直接调用importScript加载函数脚本,否则的话,通过动态创建script标签的方式进行脚本加载。为了防止在IE中的内存泄漏,在脚本加载完onload之后,要及时移除该脚本。

/**
 * util-request.js - The utilities for requesting script and style files
 * ref: tests/research/load-js-css/test.html
 */
//使用webworker加载js脚本
if (isWebWorker) {
  function requestFromWebWorker(url, callback, charset, crossorigin) {
    // Load with importScripts
    var error
    try {
      importScripts(url)
    } catch (e) {
      error = e
    }
    callback(error)
  }
  // For Developers
  seajs.request = requestFromWebWorker
}
//创建script标签的方式加载js脚本
else {
  var doc = document
  var head = doc.head || doc.getElementsByTagName("head")[] || doc.documentElement
  var baseElement = head.getElementsByTagName("base")[]

  var currentlyAddingScript

  function request(url, callback, charset, crossorigin) {
    var node = doc.createElement("script")

    if (charset) {
      node.charset = charset
    }

    if (!isUndefined(crossorigin)) {
      node.setAttribute("crossorigin", crossorigin)
    }

    addOnload(node, callback, url)

    node.async = true
    node.src = url

    // For some cache cases in IE 6-8, the script executes IMMEDIATELY after
    // the end of the insert execution, so use `currentlyAddingScript` to
    // hold current node, for deriving url in `define` call
    currentlyAddingScript = node

    // ref: #185 & http://dev.jquery.com/ticket/2709
    baseElement ?
        head.insertBefore(node, baseElement) :
        head.appendChild(node)

    currentlyAddingScript = null
  }

  function addOnload(node, callback, url) {
    var supportOnload = "onload" in node

    if (supportOnload) {
      node.onload = onload
      node.onerror = function() {
        emit("error", { uri: url, node: node })
        onload(true)
      }
    }
    else {
      node.onreadystatechange = function() {
        if (/loaded|complete/.test(node.readyState)) {
          onload()
        }
      }
    }

    function onload(error) {
      // Ensure only run once and handle memory leak in IE
      //为了防止在IE中的内存泄漏,在脚本加载完onload之后,会及时移除该脚本
      node.onload = node.onerror = node.onreadystatechange = null

      // Remove the script to reduce memory leak
      if (!data.debug) {
        head.removeChild(node)
      }

      // Dereference the node
      node = null

      callback(error)
    }
  }

  // For Developers
  seajs.request = request

}
           

三、Seajs模块依赖

Seajs的模块依赖通过方法

parseDependencies

实现,大体代码逻辑是通过使用正则匹配

require

的方式得到依赖信息。

涉及到的正则表达式如下所示:

  • /[a-z_$]/i

    :匹配所有字母,不区分大小写;
  • /^[\w$]+/

    :匹配所有字母,数字,不区分大小写;
  • /^require\s*\(\s*(['"]).+?\1\s*\)/

    :测试是否含有require(“xx”);
  • /^require\s*\(\s*['"]/

    :匹配require关键字;
  • /^[\w$]+(?:\s*\.\s*[\w$]+)*/

    :匹配第一个单词;
  • /^\.\d+(?:E[+-]?\d*)?\s*/i

    :获取.之后的整数或者是.之后的以科学计数法表达的数字;
  • /^0x[\da-f]*/i

    :匹配以0x开头[数字 a-f]的字符串;
  • /^\d+\.?\d*(?:E[+-]?\d*)?\s*/i

    :匹配小数或者以科学计数法表示的小数

源码如下所示:

/**
 * util-deps.js - The parser for dependencies
 * ref: tests/research/parse-dependencies/test.html
 * ref: https://github.com/seajs/searequire
 */
//通过正则匹配require的方式得到依赖信息
function parseDependencies(s) {
  if(s.indexOf('require') == -) {
    return []
  }
  var index = , peek, length = s.length, isReg = , modName = , parentheseState = , parentheseStack = [], res = []
  while(index < length) {
    readch()
    if(isBlank()) {
    }
    else if(isQuote()) {
      dealQuote()
      isReg = 
    }
    else if(peek == '/') {
      readch()
      if(peek == '/') {
        index = s.indexOf('\n', index)
        if(index == -) {
          index = s.length
        }
      }
      else if(peek == '*') {
        index = s.indexOf('*/', index)
        if(index == -) {
          index = length
        }
        else {
          index += 
        }
      }
      else if(isReg) {
        dealReg()
        isReg = 
      }
      else {
        index--
        isReg = 
      }
    }
    else if(isWord()) {
      dealWord()
    }
    else if(isNumber()) {
      dealNumber()
    }
    else if(peek == '(') {
      parentheseStack.push(parentheseState)
      isReg = 
    }
    else if(peek == ')') {
      isReg = parentheseStack.pop()
    }
    else {
      isReg = peek != ']'
      modName = 
    }
  }
  return res
  function readch() {
    //返回指定位置的字符
    peek = s.charAt(index++)
  }
  function isBlank() {
    return /\s/.test(peek)
  }
  function isQuote() {
    return peek == '"' || peek == "'"
  }
  //取出""或者''之间的字符,即依赖模块的名称
  function dealQuote() {
    var start = index
    var c = peek
    var end = s.indexOf(c, start)
    if(end == -) {
      index = length
    }
    else if(s.charAt(end - ) != '\\') {
      index = end + 
    }
    else {
      while(index < length) {
        readch()
        if(peek == '\\') {
          index++
        }
        else if(peek == c) {
          break
        }
      }
    }
    if(modName) {
      res.push(s.slice(start, index - ))
      modName = 
    }
  }
  function dealReg() {
    index--
    while(index < length) {
      readch()
      if(peek == '\\') {
        index++
      }
      else if(peek == '/') {
        break
      }
      else if(peek == '[') {
        while(index < length) {
          readch()
          if(peek == '\\') {
            index++
          }
          else if(peek == ']') {
            break
          }
        }
      }
    }
  }
  function isWord() {
    return /[a-z_$]/i.test(peek)
  }
  function dealWord() {
    var s2 = s.slice(index - )
      //匹配所有的a-z,A-Z,0-9  @by sxy
    var r = /^[\w$]+/.exec(s2)[]
    parentheseState = {
      'if': ,
      'for': ,
      'while': ,
      'with': 
    }[r]
    isReg = {
      'break': ,
      'case': ,
      'continue': ,
      'debugger': ,
      'delete': ,
      'do': ,
      'else': ,
      'false': ,
      'if': ,
      'in': ,
      'instanceof': ,
      'return': ,
      'typeof': ,
      'void': 
    }[r]
      //测试是否含有require("xx");
    modName = /^require\s*\(\s*(['"]).+?\1\s*\)/.test(s2)
    if(modName) {
        //匹配require关键字
      r = /^require\s*\(\s*['"]/.exec(s2)[]
      index += r.length - 
    }
    else {
      //匹配第一个单词

      index += /^[\w$]+(?:\s*\.\s*[\w$]+)*/.exec(s2)[].length - 
    }
  }
  function isNumber() {
    return /\d/.test(peek)
      || peek == '.' && /\d/.test(s.charAt(index))
  }

  function dealNumber() {
    var s2 = s.slice(index - )
    var r
    if(peek == '.') {
      //TODO: 获取.之后的整数或者是.之后的以科学计数法表达的数字
      //  /^\.\d+(?:E[+-]?\d*)?\s*/i.exec(".671+22+s2ss21")[0]   ==>.671
      r = /^\.\d+(?:E[+-]?\d*)?\s*/i.exec(s2)[]
    }
    //匹配以0x开头[数字 a-f]的字符串
    else if(/^0x[\da-f]*/i.test(s2)) {

      r = /^0x[\da-f]*\s*/i.exec(s2)[]
    }
    else {
      //匹配小数或者以科学计数法表示的小数
      r = /^\d+\.?\d*(?:E[+-]?\d*)?\s*/i.exec(s2)[]
    }
    index += r.length - 
    isReg = 
  }
}
           

四、后记

这一章由于涉及到的内容比较多,相应的介绍的也比较潦草,其实Seajs的核心代码都已经在前面几章介绍完毕了,本章着重的描写的是其中的方法实现。至此,Seajs源码分析系列到此也要告一段落了,由于接触Seajs时间并不长,对其了解并没有太深入,以上很多见解都只是初窥门径罢了。

继续阅读