Coder Social home page Coder Social logo

blog's Introduction

blog's People

Contributors

mosikoo avatar

Stargazers

 avatar  avatar

Watchers

 avatar

blog's Issues

简单编写babel-plugin

简单编写babel-plugin

基础知识点
  • AST type类型: 首先要理解抽象语法树中的类型

  • babel插件手册: 手册中陈述了大部分插件知识

  • babel处理步骤

    • 解析: babylon.parse(code, options)生成AST, 不用去了解怎么获取AST,但是一定要能看懂AST内容(简单说,给你一段code,你能写出大概AST结构)
    • 转换: 插件转换AST中节点
    • 生成: babel-generator生成代码
  • babel-types: 类似Lodash库,提供节点的方法,用的比较多的方法如下

    • 判断节点types.isXXX, 如t.isIdentifier(path.node, { name: 'xxx'})
    • 新建节点types.xxExpression, 如新建函数调用表达式t.callExpression()
  • path: 表示两节之间连接的对象,path的方法及属性很多,主要方法在这里,下面是一些常用的方法及属性

    • path.isXXX: 判断改节点的类型, 如isMemberExpression表示对象成员表达式
    • path.node表示该节点属性
    • 父节点:path.parentPath, or functionpath.findParent((path) => path.isXX())
    • 操作方法: path.replaceWith(), path.replaceWithMultiple(),path.replaceWithSourceString(),path.insertBefore(), path.insertAfter(),path.remove()
    • 停止遍历: path.skip(), path.stop()
插件基本格式
export default function ({ types: t }) { // 第一个参数为当前babel对象
  const arr = [];
  return {
    visitor: { // visitor: 插件的主要访问者
      Identifier(path, state) { // path
      	// 遍历Identifier节点
      	// 打印所有的id名
      	console.log(path.node.name);
	  },
	  FunctionDeclaration(path, state) {
        // 函数申明节点
	  },
	  ... // 其他任何expression
  }
}
编写基本插件的一个思路
  • 手动将源码用babylon.parse 转换成AST,设为AST1,
  • 同上,手动将转码后的code用babylon.parse 转换成AST,设AST2
  • 比较AST1与AST2, 得出哪些节点有差异,对有差异的节点记录并替换

Demo

要求:将Revo.aaa.bbb.ccc.ddd({params})转换成Revo('/aaa/bbb/ccc/ddd')({params})

  1. 将Revo.aaa.bbb.ccc.ddd({params})转换成AST,设为AST1
  2. 将Revo('/aaa/bbb/ccc/ddd')({params})转换成AST, 设为AST2
  3. 比较AST1与AST2,发现Revo.aaa.bbb.ccc.ddd属于MemberExpression, Revo('/aaa/bbb/ccc/ddd')属于CallExpression 类型节点
  4. 发现差异后,答案就显而易见。
    • 找到该MemberExpression
    • 创建新的CallExpression
    • 替换MemberExpression为新创建的CallExpression
贴上代码

A.对id为Revo的节点进行下手

B.递归寻找父节点,找到相应的MemberExpression

C.替换该节点

visitor: {
  Identifier(path, state) {   
    function recursion(path2) {
      const parentPath = path2.parentPath;
      if (parentPath && parentPath.isMemberExpression()) {
        const prop = parentPath.node.property.name;
        arr.push(prop);
        recursion(parentPath);
        return;
      }
      if (!arr.length) {
        return;
      } 
      // 此处为B,找到相应要替换的节点
      path2.replaceWith( // 此处为C,进行替换
        t.callExpression(
          t.identifier('Revo'),
          [t.stringLiteral(`/${arr.join('/')}${state.opts.addSuffix ? '.json' : ''}`)]
        )
      );
      path.stop();
    }
    if (t.isIdentifier(path.node, { name: 'Revo'})) {  // 此处为A
      recursion(path);
    }
  }
}

详情demo可参考这里

从Anu中探索react构建**

anu

司徒正美写的迷你React框架,通过学习它的源码可以简单的学习下react**

本文分析的是1.1.3版本, 源码在这里

本文主要是为了读者理清源代码的思路,没去了解的anu源码的同学可能很难读懂

单独模块简单讲解

createElement

主要是创建虚拟DOM

如下所示接收type, props, children,处理props中的refkey之后new Vnode(type, props, children)创建虚拟节点(对象)。

type可以为'div'等节点字符串,也可以为Component(class或stateLess function)

<div {...props}>
	{children}
</div>
// babel转义后
createElement(type, props, children)
cloneElement

cloneElement(vnode, props)

覆盖vnode中老的props,其中key值和ref需要单独处理,然后得到新的vnode

event

将大部分事件绑定到document进行委托
比如点击事件,点击触发时,从target到上层document收集绑定该事件的dom节点,
根据冒泡或捕获依次触发绑定的fn,执行完事件后会触发flushUpdater(如果执行过setState会刷新组件)

diffprops

props更新在与每次dom的mount或update之后,将props更新分style, event, class, property,依次分别更新即可

Component底层组件

初始化props, context, refs, state, 最重要的是为了继承它的setState。setState机制后续再去讲

createClass

对Componnet的二次封装,绑定除生命周期函数外的函数的this为当前Component的this, 处理mixin

shouldComponentUpdate

从Component中继承, 对shouldComponentUpdate的props或state进行浅比较

初次渲染部分

来看下大概流程图
render

初次渲染的过程比较简单(不考虑ssr)

  1. 从最开始的Vnode开始,Vnode分三种情况: text, 原生DOM, 自定义组件。
  2. 如果是text,直接渲染,createTextNode建立Text节点并返回。
  3. 如果是element节点,则创建dom,再递归遍历其Vchildren,递归完了后创建props,记录ref(这个时候只是记录,在最后的drainQueue中绑定)。
  4. 如果是Component组件,创建实例,且new Updater()(Updater用于记录实例的各个数据,更新state,渲染组件)。然后渲染子组件,子组件也需要递归渲染。
  5. 整个渲染就跟剥洋葱一样,一层一层往里渲染,一层渲染完就绑定props,记录ref。
  6. 之前渲染组件的时候会逐个push实例的Updater形成队列,这个队列作为参数在drainQueue中运行(drainQueue(queue))。 这个函数会遍历队列,从里至外绑定ref,调用componentDidMount函数,有二次渲染则进行二次渲染。

所以渲染的过程就是各种递归调用,到最后总结收尾。

更新部分

组件为什么更新?无非是调用了setState触发了更新,且父组件带动子组件更新。调用setState时,当updater._renderInNextCycle被设为true时,则在最后drainQueue中会触发refreshComponent刷新组件。

setState调用分以下几种情况(update时期)

  • 事件句柄中调用setState,设updater._renderInNextCycle = true,调用钩子函数存储脏组件,然后一次性调用drainQueue方法进行更新。
  • 生命周期钩子内执行setState,设updater._renderInNextCycle = true,在最后drainQueue被调用时进行更新。
  • 不在生命周期钩子内执行setState(一些异步的情况), 会直接调用drainQueue

anu中用于更新component的方法是refreshComponent(updater, queue),updater为更新组件的更新器,queue为更新器队列。setState都会以各种方式触发refreshComponent,或者是钩子函数(存储脏组件,然后一次性更新),或者生命周期末本身就带有是否二次render的检查。

所以我们可以暂定refreshComponent为入口,了解react刷新组件的过程。
流程图如下所示

update

流程讲解
首先得明白refreshComponent用于更新当前的组件, 生命周期从shouldComponentUpdatecomponentDidUpdate

updateComponent用于更新一个全新的组件,比如父组件更新,其子组件从componentWillReceiveProps开始渲染,到render部分,接着调用refreshComponent,才会完整的调用整个更新的生命周期。

refreshComponent为入口。
1.组件执行render前的更新生命周期钩子,然后更新render部分(这里是个递归的过程),会进入alignVnode(方法)进入Vnode更新流程。
2.Vnode更新流程,又可更渲染部分一样分三种:Text,Element,Component.
3.Text直接替换就行
4.Element,采用diff算法,type不相等则直接删除重新mount,否则回到第二点(UpdateVnode)。
5.调用componentWillReceiveProps,更新新的Vnode,Vparent,Props,context等属性,调用refreshComponent(回到了第一点)。递归完了后在push(Updater),后续有用。
6.等所有更新流程走完了之后调用drainQueue,绑定ref,调用componentDidUpdate,如果存在二次渲染,则再次进入1。

至此更新部分也讲完,更新与渲染基本差不多,只是多了diff比较的过程。

总结

react的代码与这还是有很大的出入的,虽然实现方式不一样,表达的**却没有很多的落差,熟悉其渲染及更新过程能更好的帮助我们了解react的**

webpack中事件发布订阅执行的插件架构Tapable

Tapable

是webpack中compiler的底层封装,是事件发布订阅执行的插件架构

简单模拟

function MyClass() {
	Tapable.call(this);
}

MyClass.prototype = Object.create(Tapable.prototype);

const compiler = new MyClass();

compiler.plugin('emit', function() {

}); // 类似于观察者事件中的on
  • plugin: 类似于定语-subscribe

  • applyPlugins: 类似于发布-publish

  • applyPluginsWaterfall: 瀑布串行的方式,上次执行结果作为下个函数的实参(只是替换init参数,后面参数不变),返回最终结果

  • applyPluginsBailResult: 返回值不是undefined便返回中断函数

  • applyPluginsAsync

Tapable.prototype.applyPluginsAsyncSeries = Tapable.prototype.applyPluginsAsync = function applyPluginsAsyncSeries(name) {
	var args = Array.prototype.slice.call(arguments, 1);
	var callback = args.pop();
	var plugins = this._plugins[name];
	if(!plugins || plugins.length === 0) return callback();
	var i = 0;
	var _this = this;
	args.push(copyProperties(callback, function next(err) {
		if(err) return callback(err);
		i++;
		if(i >= plugins.length) {
			return callback();
		}
		plugins[i].apply(_this, args);
	}));
	//  最后一个参数为next()函数,需要在plugin中定义next才会执行下一个plugin,这就是异步
	plugins[0].apply(this, args);
};
  • applyPluginsAsyncSeriesBailResult: 与applyPluginsAsync差不多,next函数有参数便中断执行,返回结果

  • applyPluginsAsyncWaterfall(name, init, next): 异步串行,value为第二个参数会代替init的值

  • applyPluginsParallel: 异步并行,remaining为0时说明所有异步执行完毕

  • applyPluginsParallelBailResult: 参数会依次增加,调用最后一个参数执行回调,回调的参数不为空的时候,会执行callback,且后续不会执行

webpack项目优化

webpack优化

第三方框架尽量引CDN

reactreact-dom

// webpack.config.js
module.exports = {
  ...
  externals: {
    react: 'React',
  	'react-dom': 'ReactDOM'
  }
}
  • 这样webpack在打包的时候会忽略这两个库,大大减少bundle体积
  • 这样第三方库被客户端缓存起来,增快加载速度
  • 减小bundle体积后,使得开发过程编译时间减少,增加开发效率。

引用第三方库尽量引用库中单独的包

如常用的库loadsh

有时候只用lodash中的一个深拷贝函数_.cloneDeep.

调用方法应该是

import cloneDeep from lodash-cloneeep

而不是

import cloneDeep from lodash/lib/cloneDeep

如果一些工具或组件库没有做分离,则会将整个包打包进来,影响整个bundle大小

查看chunk大小,有针对性的优化

webpack 加上--display-modules --sort-modules-by size参数

让chunks从小到大进行排列,可以看出哪些chunk体积较大,进行针对性优化

DllReference

通过DllPlugin来前置第三方包的构建

plugins: [
  new webpack.DllPlugin({
  	path: path.join(__dirname, "js", "[name]-manifest.json"),
  	name: "[name]_[hash]"
  })
]

提取CSS与第三方库

配置如下

const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  entry: {
  	main: './index.js',
  	// 公共文件
    vendors: ['react', 'react-dom'],
   	// 要提取的css放入一个js中
    'ct.style': './src/style.js'
  }
}
plugins: [
  new webpack.optimize.CommonChunkPlugin({
    name: 'vendors',
    filename: 'vendors.js',
    minChunks: 2
  }),
  new ExtractTextPlugin({
    filename: 'style.css',
    allChunks: true
  })
]

缓存

babel-loader加cacheDirectory参数进行缓存,减少rebuild时间

modules:{
  rules: [
    { use: ['babel-loader?cacheDirectory'] }
  ]
}

requirejs源码分析

requireJs简要分析

虽然目前requireJs已经不再流行,但是它的**在前端模块化历程中还是值得学习的。

翻看requireJs的源码,个人觉得基本的难点在于:

  • 它的基本逻辑比较复杂,模块依赖模块,流程互相嵌套
  • Module的状态及context的各种变量太多,让人混淆

所以本篇主要围绕着这两点去讲,理清基本的思路(抛开config各种配置)

入口

require

默认创建context,调用context.makeRequire()返回require

除了初始化的req({})req(cgf);其他js执行过程都是require(deps, callback) —>{script监听事件context.onScriptLoad —> context.completeLoad(data.id); —> 获取模块且初始化} —> 执行nextTick

define

基本用法最后只有一步 globalDefQueue.push([name, deps, callback]);,name不定义为null

然后执行script监听事件context.onScriptLoad

context

defContextName = '-',config不配置context,都只用这一个context;

变量介绍:

context = {
  config: {}, // newContext内定义
  contextName: '_',
  registry: {}, //已注册模块 registry.[id] = new Module(depMap)
  defined: defined, // 函数defined中注册的方法,保存其结果 defined[id] = exports;
  urlFetched: urlFetched, // {} urlFetched[url] = true; 表示这个url已经加载过了
  
  defQueue: defQueue, // [], 定义的队列 -- 
                          defQueue.push([name, deps, callback]); this.defined函数中
  defQueueMap: {}, 已定义队列所对应的map ,defQueueMap[name] = true;
  
  Module: Module,
  makeModuleMap: makeModuleMap,
  nextTick: req.nextTick, 
  onError: onError,
  configure(){}, // 配置config
  makeShimExports() {},
  makeRequire(){}, // 入口,返回localRequire
  enable(){},
  completeLoad(){},
  nameToUrl(){},
  load(){},
  execCb(){},
  onScriptLoad(){},
  onScriptError(){},
}

module

this.events = getOwn(undefEvents, map.id) || {};
this.map = map; // 基本信息
this.shim = getOwn(config.shim, map.id);
this.depExports = []; // 依赖包的结果存储在这里
this.depMaps = []; // 依赖包的映射信息
this.depMatched = []; 被匹配到了为true,与depExports成对应
this.pluginMaps = {};
this.depCount = 0; // 依赖模块数量

this.factory // callback
this.errback = errback;
this.inited
this.check
this.exports //本身输出

状态

  • inited: 已经初始化
  • enabled: 激活状态
  • fetched: 已经被加载
  • defined: 已定义完成,进入defined队列,且依赖的模块也都执行结束

流程图

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.