金九银十冲刺大厂,你须要知道的性能优化和手写源码

写在开头

  • 今天这些只是基础知识,面试时候若是面试很高级的岗位,只靠背面试题是很容易被识破,建议你们只是做为一个学习的点,不断去深刻、实践在项目中。有一些同窗说前端很难,不少东西,学不完,我想你多是走错了方向,毕竟人不是万能,不要太深刻那些对你目前来讲没有意义的东西,若是你有对现状不是很满意,能够在下面评论留言,我能够给你一些学习建议.
  • 个人手写源码教程集合提供给你们学习,地址是:仓库地址,但愿你们给个star~

正式开始

  • 本文为上篇-纯前端,不涉及Node.jsElectronReact-nativeLinuxDocker、数据库、redis、消息队列和操做系统等知识.根据经验,看上篇的人最多,由于毕竟这部分的前端占比大。有必定技术基础的,能够看看后面文章。

如何让display出现“动画

  • 初始化APPdisplay为"none"
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
          #app {
            width: 200px;
            height: 200px;
            background-color: red;
            display: none;
            transition: all 1s;
        }
    </style>
</head>

<body>
    <div id="app">
    </div>
    <button id="test">测试</button>
</body>

</html>
  • 初始化界面:

  • 此时我将app的display初始化为none,而且写入脚本文件
<style>
        #app {
            width: 200px;
            height: 200px;
            background-color: red;
            display: none;
        }
    </style>
    。。。
<script>
     test.onclick = function () {
        const app = document.querySelector('#app')
        console.log(app, 'app')
        app.style.transform = "translateX(200px)"
        app.style.display = "block"
    }
</script>
  • 初始化界面变成了这样:

  • 此时,我点击测试按钮

  • 并无出现动画,很是生硬的出来了,有一些场景我又要性能,好比初始化不渲染,可是当它出现又要有动画的时候,就有可能使用这行代码
test.onclick = function () {
        const app = document.querySelector('#app')
        console.log(app, 'app')
        app.style.display = "block"
        const height = app.offsetHeight
        app.style.transform = "translateX(200px)"
    }
  • 当我加入 const height = app.offsetHeight这行代码的时候,再点击测试按钮,display切换就顺带出来了“动画”,有了过分效果
  • 为何会出现动画了呢? 由于我读取dom的这些特殊属性时,浏览器就会强制清空渲染队列一次,让我拿到最新的值。也就是说读取的时候,其实已是display为"block"了,所以。咱们出现了过渡动画

  • 完整代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #app {
            width: 200px;
            height: 200px;
            background-color: red;
            display: none;
            transition: all 1s;
        }
    </style>
</head>

<body>
    <div id="app">
    </div>
    <button id="test">测试</button>
</body>
<script>
    test.onclick = function () {
        const app = document.querySelector('#app')
        console.log(app, 'app')
        app.style.display = "block"
        const height = app.offsetHeight
        app.style.transform = "translateX(200px)"
    }
</script>

</html>
  • 要注意的一点是,除了手动读取特殊属性清空浏览器渲染队列外,浏览器也会有本身的一个队列阀值,当达到后,会自动清空。这就是为何在一个for循环里面屡次操做DOM,可是它不会真的渲染那么屡次的缘由,由于浏览器帮咱们维护了一个队列,择机渲染。

前端出现性能问题可能有哪些层面的缘由?

  • 答:html

    • 网络传输层面
    • 前端渲染机制层面
    • js代码执行层面的
    • 部署层面
    • 产品设计层面
    • 前端缓存机制层面
    • 功能实现方式层面
    • 等...
  • 逐个击破:
  • 网络传输层面:对于这块,应该跟前端缓存机制层面的问题一块儿解决,咱们看掘金的代码分割:
  • 不须要看掘金的工程化环境配置,我就大概知道他们配置是怎样的。webpack/或其余工具打包,精细化分割了代码。将node_modules(vendor开头)和本身写的src(default开头)源码分开打包了.这样最小的限度的缩小了hash值改变带来的http缓存失效问题.在webpack中配置splitChunks.
splitChunks: {
    // 表示选择哪些 chunks 进行分割,可选值有:async,initial和all
    chunks: "async",
    // 表示新分离出的chunk必须大于等于minSize,默认为30000,约30kb。
    minSize: 30000,
    // 表示一个模块至少应被minChunks个chunk所包含才能分割。默认为1。
    minChunks: 1,
    // 表示按需加载文件时,并行请求的最大数目。默认为5。
    maxAsyncRequests: 5,
    // 表示加载入口文件时,并行请求的最大数目。默认为3。
    maxInitialRequests: 3,
    // 表示拆分出的chunk的名称链接符。默认为~。如chunk~vendors.js
    automaticNameDelimiter: '~',
    // 设置chunk的文件名。默认为true。当为true时,splitChunks基于chunk和cacheGroups的key自动命名。
    name: true,
    // cacheGroups 下能够能够配置多个组,每一个组根据test设置条件,符合test条件的模块,就分配到该组。模块能够被多个组引用,但最终会根据priority来决定打包到哪一个组中。默认将全部来自 node_modules目录的模块打包至vendors组,将两个以上的chunk所共享的模块打包至default组。
    cacheGroups: {
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10
        },
        // 
    default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true
        }
    }
}
  • 有人会疑问,为何会把这个问题,放在第一个?由于这是抛砖引玉,不少性能问题来源于此,小项目感知很小,可是对于一个百万行,千万行级别的巨无霸项目,提高就会很是大,这都是我实践过的。(首屏性能可能会由于精细化分割代码提高十倍,我强烈推荐你们使用精细化分割手段)
  • 代码分割完了,把依赖拆解开了,可是一个组件代码有一万多行,传输的js文件仍是有几MB大小,这个问题能忽略吗?固然不能够。咱们通常生产环境部署时候,会借助CDN,在各类云厂商提供/本身搭建的CDN服务配置相应的解析。掘金如今已由头条系接手,我看静态资源方案的域名开头使用的是s3.pstatp.com,我查询大概是跟抖音/相关的域名。或许是本身搭建的CDN服务(千万不要忽略CDN,静态资源服务对于服务器来讲是很是占带宽的,速度和体验确定会比CDN差不少,越大越重的系统提高越大
  • 网络传输层面, 仅仅是静态资源吗?前端

    • 还有前端发送ajax请求
    • 对于一些比较大的数据,例如表格、列表,咱们若是一次性拉取数据,那么就可能后端须要查询很长时间数据库、传输也须要很长时间,若是又是弱网,那就要可能用户点击一下,就要等上好几秒甚至十几秒才能拿到数据继续操做(前不久刚设计了一套混合APP改造方案,弱网/无网络状况拥有正常的体验,以为分块传输就是核心,特别在APP端)
    • 对于ws/wss协议、TCP等私有协议双工通讯的node

      • 心跳检测、发送队列持久化是必要的,手机端常常出现网络切换、弱网状况,那就避免不了心跳检测保活,咱们能够参考一种监控实现手段,他们为了不监控数据的重复上传,后端经过统一hash算法和接收到的错误上报,生成对应的后缀空文件,发布到CDN。这样前端每次上报时候,都会用统一hash算法生成对应后缀,请求那个地址,若是存在,就说明后端已经接受到了这个上报,能够丢弃这个上报。同理,咱们能够参考这个,能够用空的CDN文件作网络检测质量检测(由于一些跨平台APP框架没法精确获取当前网络质量),而后再将发送请求队列持久化便可保证请求不丢失。(对于IM场景的APP,那就要考虑更多,这里不对IM展开讲)
  • Js代码执行层面的,先看一段代码:
// 真奥义之阻塞JS主线程之休眠
const sleep = (delay = 500) => { 
 let t = Date.now(); 
 while(Date.now() - t <= delay) { 
 continue; 
 } 
};
  • 上面这段代码,会阻塞JS主解析线程,默认是500ms,这意味着,这500ms,用户操做咱们的系统,是不会有任何相应的,由于此时JS主解析线程一直被阻塞。(若是是10秒,那么会怎样?若是把这段代码换成其余计算的函数/循环,效果也是同样)。这就是由于JS代码致使的性能问题,如何解决?固然是分片分片分片!不要写太耗时的JS同步代码.
  • 这里又会有人说 用web worker wasm PWA等技术能够解决一些问题啊.可是我劝你在技术选型的时候,要考虑一个复杂度和收益的问题。如今仍是有人用JQ和原生JSCanvas开发项目,由于开发最复杂的功能,每每都须要他们。(例如富文本编辑器、可视化编辑等)
  • 对于纯粹计算占用耗时的,能够考虑web worker技术,使用事件通讯,将大量计算工做交给web worker。Worker 也能够建立新的 Worker,固然,全部 Worker 必须与其建立者同源(注意:Blink暂时不支持嵌套 Worker)。
var myWorker = new Worker('worker.js'); 
var first = document.querySelector('#number1');
var second = document.querySelector('#number2');

first.onchange = function() {
  myWorker.postMessage([first.value,second.value]);
  console.log('Message posted to worker');
}
  • web worker兼容性很好,若是是webpack环境,须要对应作一些工程化环境处理,web worker在我看来,适用于纯粹的大量计算场景。我记得有团队是用于3D数据可视化处理的,他们使用web worker后提高就很大

  • webAssembly技术,更适用于音视频领域,更底层的功能处理,例如浏览器端要音视频编解码或者图片压缩处理之类就很适合用它.它有点像浏览器版的Node.js的c++插件。Node.js能够调用C# Go Object-c等语言的插件&通讯,同理,webAssembly也是能够用各类语言编写。关于webAssembly这个技术,我写过一篇文章方便你们入门。
self.importScripts('ffmpeg.js');
onmessage = function(e) {
  console.log('ffmpeg_run', ffmpeg_run);
  var files = e.data;
  console.log(files);
  ffmpeg_run({
    arguments: ['-i', '/input/' + files[0].name, '-b:v', '64k', '-bufsize', '64k', '-vf', 'showinfo', '-strict', '-2', 'out.mp4'],
    files: files,
  }, function(results) {
    console.log('result',results);
    self.postMessage(results[0].data, [results[0].data]);
  });
}
  • 这里讲到了这些技术,不得不提到V8,因为它是自动垃圾回收机制,那么有一可能下一次回收的内存垃圾达到500MB,这个时候就会产生卡顿,有人会莫名其妙,为何会卡?由于v8垃圾回收也会阻塞主解析线程,因此形成了让你卡的假象.
这里留下一个问题,若是使用 Node.js c++插件或者 wasm技术,调用它们模块暴露的方法,它们的方法很是耗时,此时 js主解析线程挂起吗?
  • 渲染层面的性能问题:webpack

    • 频繁触发的更新,能够合并为一帧更新,异步调度处理渲染任务
function defer(fn) {
  //requestIdleCallback的兼容性很差,对于用户交互频繁屡次合并更新来讲,requestAnimation更有及时性高优先级,requestIdleCallback则适合处理能够延迟渲染的任务~
  //   if (window.requestIdleCallback) {
  //     return requestIdleCallback(fn);
  //   }
  //高优先级任务 异步的 先挂起 
  return requestAnimationFrame(fn);
}

export function enqueueSetState(stateChange, component) {
  //第一次进来确定会先调用defer函数
  if (setStateQueue.length === 0) {
    //清空队列的办法是异步执行 
    defer(flush);
  }
 
  //向队列中添加对象 key:stateChange value:component
  setStateQueue.push({
    stateChange,
    component
  });

  //若是渲染队列中没有这个组件 那么添加进去
  if (!renderQueue.some(item => item === component)) {
    renderQueue.push(component);
  }
}
  • 对于首屏渲染压力比较大的,能够采起webpack的代码分割&懒加载js文件

  • 以及图片懒加载,下拉加载更多等手段处理


  • 对于一次性渲染压力比较大并且不分页的,能够考虑使用虚拟列表,像原生APP同样,只渲染可视区域,可是给用户无感知的滚动体验。
  • 对于说很大的超复杂项目,使用可变数据,致使内存使用一直很是高的状况,可使用不可变数据,减小内存使用,我是真的使用在项目中,确实优化很大。可是前提你的项目用得上,否则就是增长复杂度(特别注意,对比跟渲染,其实都耗时,不要天真觉得用了不可变数据去作对比,而后不从新渲染组件就会提高很大性能)

  • 对于一次性渲染压力比较大并且不分页的,能够考虑使用虚拟列表,像原生APP同样,只渲染可视区域,可是给用户无感知的滚动体验。
  • 对于对于SEO和首屏要求很是高的,能够考虑作SSR,SSR有成熟的框架和库,能够考虑用nuxt,next等直接生成静态资源文件
性能是一个很是大的学问,每每都看重这方面,可是真正有对性能要求很高的项目,每每都是环境很复杂的,例如使用 JSC++插件去作解析播放同一个格式的音频,解析速度哪一个快,这些都是须要实际项目和团队支撑你去实践,这里我以为也写得比较浅,虽然说核心技术不外泄,可是这些大部分人确定都够用了

手写Promise核心原理

  • .then实现链式调用,每次都是返回一个新的Promise,跟JQuery的链式调用类似(返回this
MyPromise.prototype.then = function (onResolved, onRejected) { // 定义then      
    const self = this;
    // 指定回调函数的默认值(必须是函数)
    onResolved = typeof onResolved==='function' ?  onResolved : value => value;
    onRejected = typeof onRejected==='function' ?  onRejected : reason => {throw reason};

    return new MyPromise((resolve,reject)=>{                    // 返回一个新的MyPromise对象
      function handle(callback) {
          // 返回的MyPromise结果由onResolved/onRejected的结果决定
        // 一、抛出异常MyPromise结果为失败      reason为结果
        // 二、返回的是MyPromise               MyPromise为当前的结果
        // 三、返回的不是MyPromise             value为结果

        // 须要经过捕获获取才能知道有没有异常
        try{
          const result = callback(self.data)
          // 判断是否是MyPromise
          if ( result instanceof MyPromise){
            // 只有then才知道结果
            result.then(value=>resolve(value),reason=>reject(reason))
          }else{
            resolve(result)
          }
        }catch(error){
          reject(error)                                          // 返回的结果为reject(error)  上面第一点
        }
      }
    
      // 判断当前的status
      if (self.status === FULFILLED){                           // 状态为 fulfilled
        setTimeout(()=>{                                        // 当即执行异步回调
          handle(onResolved);
        })                                                        
      } else if (self.status === REJECTED){                     // 状态为 rejected
        setTimeout(()=>{                                        // 当即执行异步回调
          handle(onRejected);
        })
      }else{                                                    // pendding将成功和失败保存到callbacks里缓存起来
        self.callbacks.push({
          onResolved(value){                                    //函数里面调用回调函数 而且根据回调函数的结果改变MyPromise的结果
            handle(onResolved)                                  //为何这里没有setTimeout,由于上面已经写到过改变状态后回去callbacks里循环待执行的回调函数
          },
          onRejected(reason){
            handle(onRejected)
          }
        })
      }
    })
  }

手写redux核心原理

  • createStore里的实现,根据是否传入了中间件作处理
export default function createStore(reducer, enhancer) {
    if (typeof enhancer !== 'undefined') {
        return enhancer(createStore)(reducer)
    }
    let state = null
    const listeners = []
    const subscribe = (listener) => {
        listeners.push(listener)
    }
    const getState = () => state
    const dispatch = (action) => {
        state = reducer(state, action)
        listeners.forEach((listener) => listener())
    }
    dispatch({})
    return { getState, dispatch, subscribe }
}
  • 中间件实现,经过reduce,将上次的结果逐个传入,核心在于compose,支持了多个中间件使用.
import compose from './compose';
export default function applyMiddleware(...middlewares) {
    return (createStore) => (reducer) => {
        const store = createStore(reducer)
        let dispatch = store.dispatch
        let chain = []

        const middlewareAPI = {
            getState: store.getState,
            dispatch: (action) => dispatch(action)
        }
        chain = middlewares.map(middleware => middleware(middlewareAPI))
        dispatch = compose(...chain)(store.dispatch)

        return {
            ...store,
            dispatch
        }
    }
}

export default function compose(...funcs) {
    return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

手写微前端框架原理

  • 微前端的模式:

  • 微前端原理:c++

    • 经过fetch请求,经过配置的entry入口,去对应的地址拉取index.html文件,获取他们所需的资源和全部标签、DOM节点
    • 拉取他们的资源所有字符串化.
    • 把资源生成对应DOM节点和标签塞入基座中以及用key-value形式缓存内存中(避免重复发送请求拉取)
    • 像子应用同样渲染和交互
  • 代码实现:
  • 加载子应用
async function loadApp() {
  const shouldMountApp = Apps.filter(shouldBeActive);
  const App = shouldMountApp.pop();
  fetch(App.entry)
    .then(function (response) {
      return response.text();
    })
    .then(async function (text) {
      const dom = document.createElement('div');
      dom.innerHTML = text;
      const entryPath = App.entry;
      const subapp = document.querySelector('#subApp-content');
      subapp.appendChild(dom);
      handleScripts(entryPath, subapp, dom);
      handleStyles(entryPath, subapp, dom);
    });
}
  • 拉取&生成资源标签等
async function handleScripts(entryPath, subapp, dom) {
  const scripts = dom.querySelectorAll('script');
  const paromiseArr =
    scripts &&
    Array.from(scripts).map((item) => {
      if (item.src) {
        const url = window.location.protocol + '//' + window.location.host;
        return fetch(`${entryPath}/${item.src}`.replace(url, '')).then(
          function (response) {
            return response.text();
          }
        );
      } else {
        return Promise.resolve(item.textContent);
      }
    });
  const res = await Promise.all(paromiseArr);
  if (res && res.length > 0) {
    res.forEach((item) => {
      const script = document.createElement('script');
      script.innerText = item;
      subapp.appendChild(script);
    });
  }
}

手写webpack原理

  • webpack打包过程git

    • 1.识别入口文件
    • 2.经过逐层识别模块依赖。(Commonjs、amd或者es6的import,webpack都会对其进行分析。来获取代码的依赖)
    • 3.webpack作的就是分析代码。转换代码,编译代码,输出代码
    • 4.最终造成打包后的代码
  • webpack打包原理es6

    • 1.先逐级递归识别依赖,构建依赖图谱
    • 2.将代码转化成AST抽象语法树 ,一个AST抽象语法树以下所示:
    Node {
    type: 'File',
    start: 0,
    end: 32,
    loc:
     SourceLocation {
       start: Position { line: 1, column: 0 },
       end: Position { line: 1, column: 32 } },
    program:
     Node {
       type: 'Program',
       start: 0,
       end: 32,
       loc: SourceLocation { start: [Position], end: [Position] },
       sourceType: 'module',
       interpreter: null,
       body: [ [Node] ],
       directives: [] },
    comments: [] }
    • 3.在AST阶段中去处理代码
    • 4.把AST抽象语法树变成浏览器能够识别的代码, 而后输出
  • 核心实现过程
  • 将代码转化成AST,而且收集依赖
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default; 
// traverse 采用的 ES Module 导出,咱们经过 requier 引入的话就加个 .default
const babel = require('@babel/core');

const read = fileName => {
  const buffer = fs.readFileSync(fileName, 'utf-8');
  const AST = parser.parse(buffer, { sourceType: 'module' });
  console.log(AST);
  // 依赖收集
    const dependencies = {};

    // 使用 traverse 来遍历 AST
    traverse(AST, {
        ImportDeclaration({ node }) { // 函数名是 AST 中包含的内容,参数是一些节点,node 表示这些节点下的子内容
            const dirname = path.dirname(filename); // 咱们从抽象语法树里面拿到的路径是相对路径,而后咱们要处理它,在 bundler.js 中才能正确使用
            const newDirname = './' + path.join(dirname, node.source.value).replace('\\', '/'); // 将dirname 和 获取到的依赖联合生成绝对路径
            dependencies[node.source.value] = newDirname; // 将源路径和新路径以 key-value 的形式存储起来
        }
    })

    // 将抽象语法树转换成浏览器能够运行的代码
    const { code } = babel.transformFromAst(AST, null, {
        presets: ['@babel/preset-env']
    })

    return {
        filename,
        dependencies,
        code
    }

};
read('./test1.js');
  • 绘制依赖图谱
// 建立依赖图谱函数, 递归遍历全部依赖模块
const makeDependenciesGraph = (entry) => {
    const entryModule = read(entry)
    const graghArray = [ entryModule ]; // 首先将咱们分析的入口文件结果放入图谱数组中
    for (let i = 0; i < graghArray.length; i ++) {
    const item = graghArray[i];
        const { dependencies } = item; // 拿到当前模块所依赖的模块
        if (dependencies) {
            for ( let j in dependencies ) { // 经过 for-in 遍历对象
                graghArray.push(read(dependencies[j])); // 若是子模块又依赖其它模块,就分析子模块的内容
            }
        }
    }

    const gragh = {}; // 将图谱的数组形式转换成对象形式
    graghArray.forEach( item => {
        gragh[item.filename] = {
            dependencies: item.dependencies,
            code: item.code
        }
  })
  console.log(gragh)
    return gragh;
}
  • 打印gragh获得的对象:
{ './app.js':
   { dependencies: { './test1.js': './test1.js' },
     code:
      '"use strict";\n\nvar _test = _interopRequireDefault(require("./test1.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(test
1);' },
  './test1.js':
   { dependencies: { './test2.js': './test2.js' },
     code:
      '"use strict";\n\nvar _test = _interopRequireDefault(require("./test2.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(\'th
is is test1.js \', _test["default"]);' },
  './test2.js':
   { dependencies: {},
     code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports["default"] = void 0;\n\nfunction test2() {\n  console.log(\'this is test2 \');\n}\n\nvar _default = tes
t2;\nexports["default"] = _default;' } }
  • 获取编译后的代码
const generateCode = (entry) => {
    // 注意:咱们的 gragh 是一个对象,key是咱们全部模块的绝对路径,须要经过 JSON.stringify 来转换
    const gragh = JSON.stringify(makeDependenciesGraph(entry));
    // 咱们知道,webpack 是将咱们的全部模块放在闭包里面执行的,因此咱们写一个自执行的函数
    // 注意: 咱们生成的代码里面,都是使用的 require 和 exports 来引入导出模块的,而咱们的浏览器是不认识的,因此须要构建这样的函数
    return `
        (function( gragh ) {
            function require( module ) {
                // 相对路径转换成绝对路径的方法
                function localRequire(relativePath) {
                    return require(gragh[module].dependencies[relativePath])
                }
                const exports = {};
                (function( require, exports, code ) {
                    eval(code)
                })( localRequire, exports, gragh[module].code )

                return exports;
            }
            require('${ entry }')
        })(${ gragh })
    `;
}

const code = generateCode('./app.js');

console.log(code)
  • 获得编译输出的代码code以下:
(function( gragh ) {
                        function require( module ) {
                                // 相对路径转换成绝对路径的方法
                                function localRequire(relativePath) {
                                        return require(gragh[module].dependencies[relativePath])
                                }
                                const exports = {};
                                (function( require, exports, code ) {
                                        eval(code)
                                })( localRequire, exports, gragh[module].code )

                                return exports;
                        }
                        require('./app.js')
                })({"./app.js":{"dependencies":{"./test1.js":"./test1.js"},"code":"\"use strict\";\n\nvar _test = _interopRequireDefault(require(\"./test1.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_test[\"default\"]);"},"./test1.js":{"dependencies":{"./test2.js":"./test2.js"},"code":"\"use strict\";\n\nvar _test = _interopRequireDefault(require(\"./test2.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log('this is test1.js ', _test[\"default\"]);"},"./test2.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = void 0;\n\nfunction test2() {\n  console.log('this is test2 ');\n}\n\nvar _default = test2;\nexports[\"default\"] = _default;"}})
  • 复制这段代码到浏览器中运行便可

最后

  • 纸上得来终觉浅,这些手写框架&库源码,都须要一些时间去钻研,当你理解了源码以后,就会发现,源码其实大都差很少,因此你要抓住每一个框架&库源码实现最重要的点,但没有必要所有去搞懂它,抓重点学习是很重要的
  • 若是感受对你有帮助,能够点个,让更多人看到这篇文章
  • 同时,欢迎你关注个人公众号:[前端巅峰],个人gitHub源码地址是:https://github.com/JinJieTan/Peter-,记得Star