0. 准备知识
Symbol.toStringTag
Symbol.toStringTag
是一个内置 symbol,它通常作为对象的属性键使用,对应的属性值应该为字符串类型
,这个字符串用来表示该对象的自定义类型标签
,通常只有内置的 Object.prototype.toString()
方法会去读取这个标签并把它包含在自己的返回值
里。
说白了,就是希望自己创建的对象能有自己的标签类型[Object Xxxx]
。
例如,有许多JS对象类型没有toStringTag
属性,但还是能通过toString
识别特定返回的方法。
Object.prototype.toString.call('foo'); // "[object String]"
Object.prototype.toString.call([1, 2]); // "[object Array]"
Object.prototype.toString.call(3); // "[object Number]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
我们也想这样创建一个对象,用toString返回其类型标签,但是依然是[Object Object]
,咱没这待遇… 不过通过 Symbol.toStringTag
给了我们vip特权。
let obj = {};
Object.defineProperty(obj,Symbol.toStringTag,{value: 'MyTagOY'});
console.log( Object.prototype.toString.call( obj ) ) // [object MyTagOY]
class MyTag {
get [Symbol.toStringTag](){
return 'MyTagOY'
}
}
console.log( Object.prototype.toString.call( new MyTag() ) ) // [object MyTagOY]
Object.create(proto)
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
参数 proto:新创建对象的原型对象
。
Object.create(null)
创建一个非常纯洁的对象,可以自己定义自己的hasOwnProperty
、toString
等方法。使用for...in
遍历对象时,不需要在对原型属性进行检查了。
Object.create()
的原理:
if( typeof Object.create != 'function' ){
Object.create = function(proto){
function F(){}; // 构造函数
F.prototype = proto;
return new F();
}
}
getter
defineProperty
方法会直接在一个对象上定义一个新的属性,或者修改一个对象的现有属性,并返回这个对象。
复习一下 Object.defineProperty(obj, prop, descriptor)
:
obj:要在其上定义属性的对象。prop:要定义或修改的属性的名称。descriptor:属性描述符。
对于描述符:
configurable
:属性是否可配置。默认值为false。
let obj = {};
Object.defineProperty(obj,'name',{
value: 1
})
// 1. 试图去删除name属性时,属性是不可删除的。
console.log( delete obj.name ) ; console.log( Reflect.deleteProperty(o,'name') );
// 2. 试图再次去修改value, 会报错。
Object.defineProperty(obj,'name',{
value: 2
})
// TypeError: Cannot redefine property: key
// 3. 配置writable为true后,可以修改值,注意writable只可以单向从true变为false,相反会报错。
Object.defineProperty(obj,'name',{
value: 1,
writable: true
});
Object.defineProperty(obj,'name',{
value: 2
writable: false
})
console.log(obj.name) // 2
// 4. 我们从value + writable这种数据描述符,转换到get + set这种存取描述符,也会报错。
Object.defineProperty(o, 'name', {
value: 1,
writable: true
})
console.log(o.key) // 1
Object.defineProperty(o, 'name', {
get(){
return '2'
}
})
//5. 从一个存取描述符,转换到另一个存取描述符,也是报错的。
Object.defineProperty(o, 'name', {
get () {
return 1;
}
})
console.log(o.key) // 1
Object.defineProperty(o, 'name', {
get () {
return 2;
}
})
enumerable
:是否可枚举,默认false。
value
:对应的值,默认undefined。
writable
::value 是否可以被修改。默认false。
getter
:当访问该属性时,会调用此函数。默认返回undefined。
setter
:接受一个函数,当属性值被修改时,会调用此函数,并赋予新值。默认undefined。
注意:value 或 writable 和 get 或 set 键 不可同时出现。(数据描述符与存取描述符不可混用。)
扩展:proxy
可以代替 Object.defineProperty
去做事件监听(Vue)。
1. 原理
1.1 文档渲染
webpack打包后的格式:(function(){...})({'./src/index.js': (function(){...})})
;
(function(modules){
// 1. 异步加载第一步 解析入口文件
var installedModules = {};
var installedChunks = {main: 0}; // 默认只有一个代码块,并且是已加载状态0
function __webpack_require__(moduleId){
if(installedModules[moduleId]){
return installedModules[moduleId]
}
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 执行模块函数
modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);
module.l = true;
return module.exports;
}
// 2. 创建 __webpack_require__.e 函数 => 返回promise, 读取已有代码块或存新状态;创建JSONP请求。
__webpack_require__.e = function(chunkId){ // title
var installChunkData = installedChunks[chunkId]; // 获取老的代码块,刚开始只有main,是没有的title的。
let promise = new Promise(function(resolve,reject){
installChunkData = installedChunks[chunkId] = [resolve,reject]
}) // 如果调用了resolve方法,则此promise会变为成功态。
// 由于没有title代码块,所以 installChunkData => undefined -> [resolve,reject] -> [resolve,reject,promise]
installChunkData[2] = promise;
// 请求服务服务器(JSONP)
let script = document.createElement('script');
script.src = chunkId + '.bundle.js' // title.bundle.js
document.head.appendChild(script);
// 这里就会拿到 title.bundle.js 中的代码块
return promise
}
// 3. jsonArray拿到window['webpackJsonp'] 一开始是没有的,没有 赋值为 [];重新jsonArray的push方法为webpackJsonpCallBack,记得保存老的push方法,并设置this指向。
function webpackJsonpCallBack(data){ // 读取异步导入文件,合并到modules上;installedChunks 赋予该模块状态。
let chunkIds = data[0]; // ['script']
let moreModules = data[1]; // {'./src/title.js': (function(){...})}
let resolves = []; // 保存值
// 给installedChunks赋值title, 并标示状态。
for( let i = 0 ; i < chunkIds.length ; i++ ){
let chunkId = chunkIds[i];
resolves.push(installedChunks[chunkId][0]) //这时还是installedChunks[chunkId]还是[resolve,reject,promise] (保存)
installedChunks[chunkId] = 0 // 已经加载成功。(改变)
}
// 将moreModules(对象)与 modules进行合并 => {'./src/index.js':... ; './src/title.js':...}
// 合并到modules上的原因是 可以用__webpack_require__执行对应的模块函数了
for (const moduleId in moreModules) {
modules[moduleId] = moreModules[moduleId]
};
while( resolves.length ){
resolves.shift()() // 取数第一个,并执行。代表着promise的成功,会走下一个then
};
if(parentJsonFunction){
// 虽然把数组的push重写了,但是老的push方法也得保留在parentJsonFunction。确保data放到数组里,必须绑定this。
parentJsonFunction(data)
}
};
var jsonArray =( window['webpackJsonp'] = window['webpackJsonp'] || [] );
var oldJsonpFunction = jsonArray.push.bind(jsonArray);
// 重新jsonArray的push方法,重新复制为webpackJsonpCallBack;在 title.bundle.js 中的push实际上是webpackJsonpCallBack方法。
jsonArray.push = webpackJsonpCallBack;
var parentJsonFunction = oldJsonpFunction; // 保留 老数组的push方法。
// 4. __webpack_require__.t
__webpack_require__.t = function(value,mode){ //mode 为什么要用二进制判断,十分方便。 7 对应二进制 111
value = __webpack_require__(value); // 返回字符串'title'
// 兼容 es6Module,保证 __webpack_require__.t函数返回的值中 必须带有'default',不然还得判断是否为es6模块,是es6模块要用'default'取值,不是则直接获取。
// 补充:commomjs => module.exports = 'title' => 'title'
// ex6Module => export default 'title' => {default: 'title'} 两种模式取值是不同的。
let ns = Object.create(null);
Object.defineProperty(ns,'__esModule',{value: true}); // __esModule表示这是es6模块语法
Object.defineProperty(ns,'default',{value});
return ns; // {__esModule: true,default: 'title'}
}
return __webpack_require__('./src/index.js');
})
({
'./src/index.js': (function(){ // 入口文件
function (module, exports, __webpack_require__) {
let button = document.createElement('button');
button.innerHTML = '点击';
button.addEventListener('click', () => {
__webpack_require__
.e("title") // e函数:加载‘title’代码块。
.then(__webpack_require__.t.bind(null, "./src/title.js", 7)) // 作用是保证 result肯定是一个对象 并且有'title'属性。
.then(result => { // {__esModule: true,default: 'title'}
console.log(result.default) // title
})
})
document.body.appendChild(button)
}
})
})
// title.config.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["title"], {
"./src/title.js":
(function (module, exports) {
module.exports = 'title'
})
}]);
1.2 loader
所有的loader的执行顺序都有两个阶段:pitching阶段
和normal阶段
,类似于js中的事件冒泡、捕获阶段。
Pitching阶段
: post,inline,normal,pre
Normal阶段
:pre,normal,inline,post
pitch / normal 每个完整的loader都有一个pitch,首先是从左往右执行 pitch没有返回值时,继续执行右面的pitch,当一旦遇到 带返回值的pitch,将不再执行后面的loader,直接跳到第一个loader,将pitch的返回值传给source,执行,然后结束。
loader类型
loader 的来源有两个:行内与配置文件
由于loader有执行顺序,有的loader必须第一个执行,有的必须在最后执行。
loader的叠加顺序 = post(后置) + inline(内联) + normal(正常)+ pre(前置)。
前缀的含义:
- 行内loader:一般写在路径里。
let request = 'inline-loader1!inline-loader2!./style.css';
- 前置loader,设置
enforce
为 pre
当不写enforce
时,为 normal 正常。
let rules = [
{test: /\.css$/,enforce: 'pre',use: ['pre-loader1','pre-loader2']}
{test: /\.css$/,enforce: 'pre',use: ['pre-loader1','pre-loader2']}
{test: /\.css$/,enforce: 'pre',use: ['pre-loader1','pre-loader2']}
]
- 正常loader
let rules = [
{test: /\.css$/,use: ['normal-loader1','normal-loader2']}
{test: /\.css$/,use: ['normal-loader1','normal-loader2']}
]
- 后置loader,设置
enforce
为 post
let rules = [
{test: /\.css$/,enforce: 'post',use: ['post-loader1','post-loader2']}
{test: /\.css$/,enforce: 'post',use: ['post-loader1','post-loader2']}
]
得到一堆loader的绝对路径
let path = require('path');
let nodeModules= path.resolve(__dirname,"node_modules");
let request = "!!inline-loader1!inline-loader2!./style.css"; //行内
let rules = [
{test: /\.css$/,enforce: 'pre',use: ["pre-loader1","pre-loader2"]}, // 前置
{test: /\.css$/,use: ["normol-loader1","normol-loader2"]}, // 正常
{test: /\.css$/,enforce: 'post',use: ["post-loader1","post-loader2"]} // 后置
];
//-------------------------
// 不要pre和普通loader,只剩下 inline + post
const noPreAutoLoaders = request.startsWith("-!")
// 不要普通loaders
const noAutoLoaders = noPreAutoLoaders || request.startsWith("!");
// 不要pre post 普通,只剩下inline
const noPerPostAutoLoaders = request.startsWith("!!");
//-----------------------
let inlineLoaders = request.replace(/^-?!+/,"")
.replace(/!!+/g,"!")
.split("!"); // [inline-loader1,inline-loader2,./style.css]
let resource = inlineLoaders.pop(); // ./style.css
//-----------------------
// 经过这个映射,是把一个loader模块名变成一个绝对路径数组。
let resolveLoader = loader => path.resolve(nodeModules,loader + '.js')
let preLoaders = [];
let postLoades = [];
let normalLoaders = [];
for( let i = 0 ; i < rules.length ; i++ ){
let rule = rules[i];
if( rule.test.test(resource) ){
if( rule.enforce === 'pre' ){
preLoaders.push(...rule.use);
}else if(rule.enforce === 'post'){
postLoades.push(...rule.use);
}else{
normalLoaders.push(...rule.use)
}
}
}
let loaders;
if(noPerPostAutoLoaders){
loaders = [...inlineLoaders]
}else if(noPreAutoLoaders){
loaders = [...postLoades,...inlineLoaders]
}else if(noAutoLoaders){
loaders = [...postLoades,...inlineLoaders,...preLoaders]
}else {
loaders = [...postLoades,...inlineLoaders,...normalLoaders,...preLoaders];
}
loaders = loaders.map(resolveLoader) // 将loader全部转为绝队路径
console.log(loaders)
loader-runner
读取 要加载的资源文件(src/index.js),传给loader,一个一个的去加工,最终返回加工后的结果。
let path = require('path');
const fs = require('fs');
function createLoaderObject(loader){
let loaderObj = {data: {}}; // data后面有用
loaderObj.request= loader; // loader的绝对路径
loaderObj.normal = require(loader); // 拿到loader对应函数代码块
loaderObj.pitch = loaderObj.normal.pitch;
return loaderObj;
};
function runLoader(options,callback){
let loaderContext = {} // 这个对象最终会成为loader函数的this
let resource = options.resource // 要加载的资源路径
let loaders = options.loaders // 要使用的loader模块 [loader1,loader2,loader3]
loaders = loaders.map(createLoaderObject); // [{normal:...,pitch:...},{normal:...,pitch:...},...]
loaderContext.loaderIndex = 0; // 当前的索引
loaderContext.readResource = fs; //读取资源的方法是readerFile方法
loaderContext.resource = resource;
loaderContext.loaders = loaders;
iteratePitchingLoader(loaderContext,callback);
function processResource(loaderContext,callback){
let buffer = loaderContext.readResource.readFileSync(loaderContext.resource,'utf-8');
loaderContext.loaderIndex--;
iterateNormalLoader(loaderContext,buffer,callback)
};
function iterateNormalLoader(loaderContext,args,callback){
if( loaderContext.loaderIndex < 0 ){
return callback(null,args);
}
let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
let normalFn = currentLoaderObject.normal;
let result = normalFn.call(loaderContext,args); // loader n 的返回值是loader n-1 的参数。
loaderContext.loaderIndex--;
iterateNormalLoader(loaderContext,result,callback);
};
function iteratePitchingLoader(loaderContext,callback){
if(loaderContext.loaderIndex >= loaderContext.loaders.length){ // 当索引大于loader个数时
return processResource(loaderContext,callback); // 处理index.js文件
};
let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; // 拿到第一个loader
let pitchFn = currentLoaderObject.pitch;
if(!pitchFn){
loaderContext.loaderIndex++
return iteratePitchingLoader(loaderContext,callback)
}
let result = pitchFn.apply(loaderContext);
if(result){
loaderContext.loaderIndex--
iterateNormalLoader(loaderContext,result,callback)
}else{
loaderContext.loaderIndex++
iteratePitchingLoader(loaderContext,callback)
}
}
}
let entry = './src/index.js';
let options = {
resource: path.resolve(__dirname,entry),
loaders: [ // 用这三个loader去加载entry文件,也就是上一个标题代码返回的结果。
path.resolve(__dirname,'dist注释/loader/loaders/loader1.js'),
path.resolve(__dirname,'dist注释/loader/loaders/loader2.js'),
path.resolve(__dirname,'dist注释/loader/loaders/loader3.js')
]
}
runLoader(options,(err,result)=>{
console.log('执行完毕')
console.log(result) // loader2patch//1
})