Git Product home page Git Product logo

note-book's Introduction

  • 👋 Hi, I’m @tianma630
  • 👀 I’m interested in ...
  • 🌱 I’m currently learning ...
  • 💞️ I’m looking to collaborate on ...
  • 📫 How to reach me ...

note-book's People

Contributors

tianma630 avatar

Watchers

 avatar  avatar

note-book's Issues

vue如何实现对数组的动态响应?

因为Object.defineProperty()只支持对象的属性劫持,所以数组是如何实现动态响应的呢?我们看一下vue的代码

var arrayProto = Array.prototype;  // 获取数组的原型,原型中包含了数组所有的属性、方法
var arrayMethods = Object.create(arrayProto); // 用数组的原型创建一个新的对象示例

var methodsToPatch = [ // 枚举了所有可能改变数组自身的方法
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) { // 遍历枚举的方法
  // cache original method
  var original = arrayProto[method]; // 获取方法名称 比如push
  def(arrayMethods, method, function mutator () { // 劫持数组中的方法,问题1: 为啥劫持的是arrayMethods, 而不是arrayProto
    var args = [], len = arguments.length;
    while ( len-- ) args[ len ] = arguments[ len ]; // 把参数放入数组args,问题2: 是否多此一举

    var result = original.apply(this, args); // 执行该方法,也可以用arguments
    var ob = this.__ob__;  // 获取observer,里面存储了dep,dep中存储了watcher
    var inserted;
    switch (method) { // 获取数组中新增的元素,比如list.push(item):item即是新的元素
      case 'push':
      case 'unshift':
        inserted = args;
        break
      case 'splice':
        inserted = args.slice(2);
        break
    }
    if (inserted) { ob.observeArray(inserted); } // 劫持新增的元素
    // notify change
    ob.dep.notify(); // 执行watcher,触发生成vdom并patch
    return result // 返回数组方法执行结果
  });
});

在上面的代码注解中提到了2个问题:

  1. 问题1其实是为了不劫持原生数组对象,因为在一个页面中我们可能会有一些代码逻辑不在vue的环境中,如果把Array.prototype给劫持了,vue环境内外的数组操作都会被劫持,这并不是我们想要的。
  2. 问题2可能只是为了方便操作数组,比如slice方法

看了问题1 的解释,既然劫持的不是Array.prototype,那我们在操作数组时是怎么进的上面的逻辑的。我们可以看一下observe方法是如何实现的

var Observer = function Observer (value) {
  this.value = value; // 对象or数组
  this.dep = new Dep(); // 在oberver实例中创建dep
  this.vmCount = 0;
  def(value, '__ob__', this); // 定义value的__ob__属性为当前的oberver实例,对应上面的代码是如何获取observer的
  if (Array.isArray(value)) { // 如果是数组
    if (hasProto) {
      protoAugment(value, arrayMethods); // 将value的原型对象修改为arrayMethods,原来如此,进一步解释了上面的问题1
    } else {
      copyAugment(value, arrayMethods, arrayKeys);
    }
    this.observeArray(value); // 如果是数组,劫持递归劫持
  } else {
    this.walk(value); // 实现对象的属性劫持
  }
};

function protoAugment (target, src) { // 修改原型实例方法
  /* eslint-disable no-proto */
  target.__proto__ = src;
  /* eslint-enable no-proto */
}

到这里,我们就应该就明白了,vue是如何只劫持vue环境中的数组操作,而不影响vue环境外的数组操作。

event loop

可以参考下面的视频和文章
https://zhuanlan.zhihu.com/p/122013710
https://www.bilibili.com/video/BV1E441197g5
https://www.bilibili.com/video/BV13E411B7yp

个人的几个理解

  1. evet loop并不是javascript引擎提供(比如v8),因为它就是单线程的,而是浏览器为了实现更好的用户体验模拟的一种类似多线程的技术。
  2. 也是基于上面的原因,micro task 和 macro task的api都是web api,而不是javascript自带。比如promise、mutationObserver、setTimeout、setInterval。
  3. micro task 一次循环会全部执行完,macro task 一次循环只能执行一个。上面的第一个视频中有一个页面卡死的例子就是这个原因。
// 卡死
while(true);
// 不会卡死
function loop() {
  setTimeout(() => {}, 0);
}
loop();
  1. vue中的nexttick方法就是用promise来实现batch update。还比如:可以将一个页面上相同接口的请求就行合并后在请求。

react vdom diff

传统diff

所谓diff算法,就是给定任意两棵树,找到最少的转换步骤。传统的的Diff 算法复杂度需要O(n^3)。

react diff

react总结Web UI的特点,将diff算法的复杂度提升到O(n),主要依据以下3个策略。

  1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
  2. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
  3. 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。

基于上面的原则,react从tree diff、component diff、element diff三个方面对diff算法就行了优化。

tree diff

基于策略1,react对整颗树进行比对的时候,只需要将同层级的节点进行比对。

如上图所示,同颜色框中的节点才会进行比较,只要发现节点不存在,就会将该节点整个删除。

component diff

基于策略2,react会比对组件的类型,只要类型不同,就会将组件整个删除;如果发现是相同的组件,就会将组件下的节点进行比对。

element diff

统一层级下面的接口进行比对的时候,有3种情况

  1. 删除节点
  2. 新增节点
  3. 替换节点

react会顺序对比相同位置上节点,如果节点不同则会新增节点,并删除老的节点

function enqueueInsertMarkup(parentInst, markup, toIndex) {
  updateQueue.push({
    parentInst: parentInst,
    parentNode: null,
    type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
    markupIndex: markupQueue.push(markup) - 1,
    content: null,
    fromIndex: null,
    toIndex: toIndex,
  });
}

function enqueueMove(parentInst, fromIndex, toIndex) {
  updateQueue.push({
    parentInst: parentInst,
    parentNode: null,
    type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
    markupIndex: null,
    content: null,
    fromIndex: fromIndex,
    toIndex: toIndex,
  });
}

function enqueueRemove(parentInst, fromIndex) {
  updateQueue.push({
    parentInst: parentInst,
    parentNode: null,
    type: ReactMultiChildUpdateTypes.REMOVE_NODE,
    markupIndex: null,
    content: null,
    fromIndex: fromIndex,
    toIndex: null,
  });
}

key

为了实现对原有节点的重复使用,用key来表示是否是相同的节点。

_updateChildren: function(nextNestedChildrenElements, transaction, context) {
  var prevChildren = this._renderedChildren;
  var nextChildren = this._reconcilerUpdateChildren(
    prevChildren, nextNestedChildrenElements, transaction, context
  );
  if (!nextChildren && !prevChildren) {
    return;
  }
  var name;
  var lastIndex = 0;
  var nextIndex = 0;
  for (name in nextChildren) {
    if (!nextChildren.hasOwnProperty(name)) {
      continue;
    }
    var prevChild = prevChildren && prevChildren[name];
    var nextChild = nextChildren[name];
    if (prevChild === nextChild) {
      // 移动节点
      this.moveChild(prevChild, nextIndex, lastIndex);
      lastIndex = Math.max(prevChild._mountIndex, lastIndex);
      prevChild._mountIndex = nextIndex;
    } else {
      if (prevChild) {
        lastIndex = Math.max(prevChild._mountIndex, lastIndex);
        // 删除节点
        this._unmountChild(prevChild);
      }
      // 初始化并创建节点
      this._mountChildAtIndex(
        nextChild, nextIndex, transaction, context
      );
    }
    nextIndex++;
  }
  for (name in prevChildren) {
    if (prevChildren.hasOwnProperty(name) &&
        !(nextChildren && nextChildren.hasOwnProperty(name))) {
      this._unmountChild(prevChildren[name]);
    }
  }
  this._renderedChildren = nextChildren;
},
// 移动节点
moveChild: function(child, toIndex, lastIndex) {
  if (child._mountIndex < lastIndex) {
    this.prepareToManageChildren();
    enqueueMove(this, child._mountIndex, toIndex);
  }
},
// 创建节点
createChild: function(child, mountImage) {
  this.prepareToManageChildren();
  enqueueInsertMarkup(this, mountImage, child._mountIndex);
},
// 删除节点
removeChild: function(child) {
  this.prepareToManageChildren();
  enqueueRemove(this, child._mountIndex);
},

_unmountChild: function(child) {
  this.removeChild(child);
  child._mountIndex = null;
},

_mountChildAtIndex: function(
  child,
  index,
  transaction,
  context) {
  var mountImage = ReactReconciler.mountComponent(
    child,
    transaction,
    this,
    this._nativeContainerInfo,
    context
  );
  child._mountIndex = index;
  this.createChild(child, mountImage);
},

参考

React 源码剖析系列 - 不可思议的 react diff

webpack-如何实现模块化打包

按照本文原理,手写了个打包工具:https://github.com/tianma630/mypack

首先我编写了3个js文件
a.js

export default 1;

export const b = 2;

export const c = 3;

export const plus = function(a, b) {
  return a + b;
}

b.js

export default function (a) {
  return  a * a;
};

entry.js

import a, {b, c, plus} from './a';
import d from './b';

let ret = plus(a, b) + d(c);

console.log(ret);

使用webpack命令生成打包文件,结果其实是一个直接执行函数,为了方便查看,删除了多余代码

(function(modules) {
	// 存储了所有模块化的信息
	var installedModules = {};
	// 按文件处理解析模块化信息,moduleId其实就是文件路径名称
	function __webpack_require__(moduleId) {
		// 检查该文件是否缓存了
		if(installedModules[moduleId]) {
			return installedModules[moduleId].exports;
		}
		// 新建一个空的
		var module = installedModules[moduleId] = {
			i: moduleId, // 模块id 其实就是文件路径,比如./src/b.js
			l: false,
			exports: {} // 这个文件中的所有export的内容,比如b.js中 {default: function(a) { return a * a; }}
		};
		// 处理文件中的模块化信息,并执行代码,具体逻辑在下面的参数中,先mark下
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
		// 标记文件被加载,就是是否被使用
		module.l = true;
		// 返回模块化的信息
		return module.exports;
	}
	// 缓存一个模块,就是处理类似export const b = 2;的逻辑
	__webpack_require__.d = function(exports, name, getter) {
		if(!__webpack_require__.o(exports, name)) { // 判断下这个模块是否缓存过
			Object.defineProperty(exports, name, { enumerable: true, get: getter });
		}
	};
         // 入口执行函数,就是执行entry中的配置文件
	return __webpack_require__(__webpack_require__.s = "./src/entry.js");
})
({
// 处理具体的模块代码文件,按文件区分模块,执行的入口就是上面mark的call方法
  "./src/a.js": (function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    // 为了方便查看eval中的代码,我们在下面格式化一下
    eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"b\", function() { return b; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"c\", function() { return c; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"plus\", function() { return plus; });\n/* harmony default export */ __webpack_exports__[\"default\"] = (1);\n\nconst b = 2;\n\nconst c = 3;\n\nconst plus = function(a, b) {\n  return a + b;\n}\n\n//# sourceURL=webpack:///./src/a.js?");
  }),
  
  "./src/b.js": (function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (function (a) {\n  return  a * a;\n});;\n\n//# sourceURL=webpack:///./src/b.js?");
  }),
  
  "./src/entry.js": (function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ \"./src/a.js\");\n/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./b */ \"./src/b.js\");\n\n\n\nlet ret = Object(_a__WEBPACK_IMPORTED_MODULE_0__[\"plus\"])(_a__WEBPACK_IMPORTED_MODULE_0__[\"default\"], _a__WEBPACK_IMPORTED_MODULE_0__[\"b\"]) + Object(_b__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_a__WEBPACK_IMPORTED_MODULE_0__[\"c\"]);\n\nconsole.log(ret);\n\n\n\n// (function(a, b) {\n//   let ret = a(1, 2) + b(2, 3);\n\n//   console.log(ret);\n// })(\n// {a: function(a, b) {\n//   return a + b;\n// }}, \n// function(a, b) {\n//   return a * b;\n// }\n// )\n\n\n//# sourceURL=webpack:///./src/entry.js?");
  })
  
});

格式化eval中的代码

// function(module, __webpack_exports__, __webpack_require__)

// __webpack_require__ 上面的模块化入口方法
// __webpack_exports__ 存储模块化信息的对象, 可以理解为{}

// a.js
__webpack_require__.r(__webpack_exports__); // 标记了这个是存储模块化信息的对象,没有直接作用
__webpack_require__.d(__webpack_exports__, "b" , function() { return b; }); // 放入到模块化对象中,可以理解为{b: function() { return b; }}
 __webpack_require__.d(__webpack_exports__, "c", function() { return c; });
 __webpack_require__.d(__webpack_exports__, "plus", function() { return plus; });
 __webpack_exports__["defalut"] = (1); // 处理export default
 const b = 2;
 const c = 3;
 const plus = function(a, b) {
     return a + b;
    }


// b.js
__webpack_require__.r(__webpack_exports__);
__webpack_exports__["default"] = (function (a) {
     return  a * a;
});


// entry.js
// 入口文件必须放在最下面执行
__webpack_require__.r(__webpack_exports__);
var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/a.js");  // 根据路径获取模块依赖,如果模块不存在,则先执行./src/a.js中的代码,如果已经执行过了,则直接获取缓存中的依赖
var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./src/b.js");

// 将依赖对象和方法动态替换成引入的依赖
let ret = Object(_a__WEBPACK_IMPORTED_MODULE_0__["plus"])(_a__WEBPACK_IMPORTED_MODULE_0__["default"], _a__WEBPACK_IMPORTED_MODULE_0__["b"]) + _a__WEBPACK_IMPORTED_MODULE_0__["c"] * _b__WEBPACK_IMPORTED_MODULE_1__["default"];

console.log(ret);

模块化整体的逻辑可以简单理解为,按js代码文件路径,将所有的export缓存到模块化对象installedModules中,在入口文件,按依赖路径从模块化对象中获取依赖并执行。

// 本文的例子生成的installedModules对象
{ 
  "./src/entry.js": { 
    "i": "./src/entry.js", 
    "l": true, 
    "exports": {}
  },
  "./src/a.js": { 
    "i": "./src/a.js",
    "l": true,
    "exports": {
      "b": function () {
        return b;
      },
      "c": function() {
        return c;
      },
      "plus": function (a, b) {
        return a + b;
      },
      "default": 1
    }
  },
  "./src/b.js": { 
    "i": "./src/b.js",
    "l": true,
    "exports": {
      "default": function (a) {
        return a * a;
      }
    } 
  }
}

最后还有一个问题是如何把实际的代码转变成eval中的代码结构的,这就要用到ast了,有兴趣的同学可以去了解下。

webpack-代码分割的原理

查看官方指南关于代码分割的文档,我们可以知道,实现代码分割有3种方式

  1. 入口起点:使用 entry 配置手动地分离代码。
  2. 防止重复:使用 CommonsChunkPlugin 去重和分离 chunk。
  3. 动态导入:通过模块的内联函数调用来分离代码。

前2种是陪过配置实现,第3种是通过代码动态分析实现,但是最后代码生成的结果是一样的,这里我们就那第3种方式为例。

动态导入也有2种写法,一种是基于ESMA提案的import()方法,另一种是webpack提供的require.ensure方法,这里选择第一种语法,实现一个例子。

index.js

setTimeout(() => {
    import(/* webpackChunkName: "print" */ './print').then((print) => {
        print.log();
    })
}, 3000);

print.js

export function log() {
    console.log('print log');
}

在webpack的配置的ouput选项中需要加上chunkFilename配置,用于生成需要动态导入的文件。

module.exports = {
    entry: {
        app: './src/index.js'
    },
   ...
    output: {
        filename: '[name].bundle.js',
        chunkFilename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
};

执行webpack打包命令,可以看到最终生成了2个文件

  1. app.bundle.js
  2. print.bundle.js

在chrome中运行可以发现,先引入了app.bundle.js,3秒钟后引入了print.bundle.js。下面我们看下这个过程是怎么实现的。

先看下app.bundle.js的代码,删减后的代码

(function(modules) { // webpackBootstrap
	// 动态引入的方法
	__webpack_require__.e = function requireEnsure(chunkId) {
        // 因为动态引入的过程是异步的,所以promise化
		var promises = [];


        var installedChunkData = installedChunks[chunkId];
        // 是否已经引入过, 0表示已经引入
		if(installedChunkData !== 0) { // 0 means "already installed".

			// 是否已经创建了promise
			if(installedChunkData) {
				promises.push(installedChunkData[2]);
			} else {
				// 创建一个promise
				var promise = new Promise(function(resolve, reject) {
					installedChunkData = installedChunks[chunkId] = [resolve, reject];
				});
				promises.push(installedChunkData[2] = promise);

				// 通过动态创建script引入js文件
				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);
	};

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

"./src/index.js":
(function(module, exports, __webpack_require__) {

    eval(```
        setTimeout(() => {
            // 动态引入print文件
            __webpack_require__.e("print")
            // 引入之后执行__webpack_require__,模块化处理(print文件是app文件的一个模块,具体逻辑可以参考之前的文章:https://github.com/tianma630/note-book/issues/9)
            .then(__webpack_require__.bind(null,"./src/print.js"))
            // 执行代码
            .then((print) => {
                print.log();
            })
        }, 3000);
        ```
    )
})
    
});

最后可以总结出,代码分割的执行流程可以分为3个步骤

  1. 动态创建script标签,引入被分割的js文件
  2. 调用模块化方法
  3. 执行业务逻辑代码

react scheduler

scheduler模块react为了实现fiber架构新增的一个模块,主要用来实现任务的拆分和调度执行。

scheduler是一个独立的模块,暴露了一些任务调度相关的方法,可以直接调用。

我们可以从npm仓库直接安装使用:

const scheduler = require('scheduler')

scheduler.unstable_scheduleCallback(scheduler.unstable_ImmediatePriority, () => {
    console.log(111)
})

scheduler.unstable_scheduleCallback(scheduler.unstable_NormalPriority, () => {
    console.log(222)
})

scheduleCallback可以认为是 scheduler 的入口方法,用于添加任务。这个方法主要有2个参数

  1. priorityLevel:任务的优先级
  2. callback:回调,任务的具体逻辑

scheduler 根据重要程度给任务分成了5个优先级

  • NoPriority = 0;
  • ImmediatePriority = 1;
  • UserBlockingPriority = 2;
  • NormalPriority = 3;
  • LowPriority = 4;
  • IdlePriority = 5;

除了 NoPriority表示最低的优先级外,剩余的5个数值越小优先级越高。

下面看一下scheduleCallback方法

function unstable_scheduleCallback(priorityLevel, callback, options) {
  var currentTime = getCurrentTime(); // 通过performance.now()获取当前时间

  // 判断 options 参数中是否有延迟字段( delay ),有的话把 currentTime 和 delay 相加
  var startTime;
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }

  // 根据不同的优先级获取对应的超时时间,优先级越高,超时时间就越小,表示执行的越早
  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = IMMEDIATE_PRIORITY_TIMEOUT;
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT;
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT;
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT;
      break;
  }

  // 这里需要说一下延迟时间(delay)和超时时间(timeout)的区别,延迟时间是业务定义的必须执行等待的;
  // 而超时时间是根据任务优先级映射的时间,主要作用是给任务按优先级排序执行,只要没有高优先级的任务了,低优先级的任务也会立即执行。

  // 当前时间(currentTime) + 延迟时间(delay) + 超时时间(timeout) = 任务的过期时间
  var expirationTime = startTime + timeout;

  // 新建一个任务
  var newTask = {
    id: taskIdCounter++, // 递增的id
    callback,            // 业务逻辑 
    priorityLevel,       // 优先级 
    startTime,           // 任务开始执行时间 currentTime + delay
    expirationTime,      // 过期时间 currentTime + delay + timeout
    sortIndex: -1,       // 排序索引
  };

  // 所有profile相关的都是用于代码调试,可忽略
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  if (startTime > currentTime) { // 有延迟(delay)的情况
    // This is a delayed task.
    newTask.sortIndex = startTime; // 设置排序索引为开始时间,通过开始时间来给任务的执行排序
    push(timerQueue, newTask); // 把任务放入到timerQueue,先简单任务是把任务放入数组中
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) { // 如果没有可执行的任务,并且上面新创建的任务是下一次待执行的任务
    
      // 如果最近的一个任务需要延迟执行,则通过setTimeout方法,暂停delay时间后执行任务。主要用于性能优化,在没有任务时暂停代码运行
      // 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.
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else { // 没有延迟(delay)的情况,说明当前肯定有任务需要执行
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    // 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); // 在下一个 event loop 执行 flushWork 方法
    }
  }

  return newTask;
}

下面我们看一下 requestHostCallback 方法

function requestHostCallback(callback) {
  scheduledHostCallback = callback; // 把 flushWork 方法赋给 scheduledHostCallback
  if (!isMessageLoopRunning) { // 是否有任务正在执行
    isMessageLoopRunning = true;
    port.postMessage(null); // Scheduler 首选用 MessageChannel 调度下一个 event loop,在不支持 MessageChannel 的环境,或 nodejs 环境也 polyfill 了 setTimeout 的实现版本。主要的原因是 setTimout 方法自带了几毫秒的延时,在有大量的任务时会有比较大的性能问题
  }
}

具体的 MessageChannel 的应用

const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline; // 当收到 postMessage 方法时会调用 performWorkUntilDeadline

每次 event loop 在 performWorkUntilDeadline 方法中处理

const performWorkUntilDeadline = () => {
  if (scheduledHostCallback !== null) { // scheduledHostCallback 表示 flushWork
    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; // yieldInterval 表示每个时间片可以执行任务的时间长度,默认5毫秒,可以通过 forceFrameRate 方法动态计算;在每个时间片内还会有多次 postMessage
    const hasTimeRemaining = true; // 是否还有剩余时间
    try {
      const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime); // 执行 flushWork 方法
      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); // 如果还有可执行的任务 postMessage 递归调用 performWorkUntilDeadline 和 flushWork 方法
      }
    } 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;
};

下面看一下 flushWork 方法

function flushWork(hasTimeRemaining, initialTime) {
  if (enableProfiling) {
    markSchedulerUnsuspended(initialTime);
  }

  // We'll need a host callback the next time work is scheduled.
  isHostCallbackScheduled = false; // 取消暂停调度
  if (isHostTimeoutScheduled) { // 把延迟暂停取消
    // We scheduled a timeout but it's no longer needed. Cancel it.
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

  isPerformingWork = true; // 标识正在执行任务
  const previousPriorityLevel = currentPriorityLevel; // 缓存当前的优先级
  try {
    if (enableProfiling) {
      try {
        return workLoop(hasTimeRemaining, initialTime);
      } catch (error) {
        if (currentTask !== null) {
          const currentTime = getCurrentTime();
          markTaskErrored(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        throw error;
      }
    } else {
      // No catch in prod code path.
      return workLoop(hasTimeRemaining, initialTime);  // 具体执行任务
    }
  } finally {
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel; 
    isPerformingWork = false; // 标识执行任务结束
    if (enableProfiling) {
      const currentTime = getCurrentTime();
      markSchedulerSuspended(currentTime);
    }
  }
}

workLoop 方法

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime; // 执行任务开始时间
  advanceTimers(currentTime); // 把不再有延迟的任务(delay时间到了)从 timerQueue 放入到 taskQueue
  currentTask = peek(taskQueue); // 从 taskQueue 按排序取下一个需要执行的任务
  while (
    currentTask !== null && // 任务不为空
    !(enableSchedulerDebugging && isSchedulerPaused) // 这2个参数用于debug
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost()) // 任务未过期 && (没有剩余时间 || 因为其他原因必须暂停)
    ) { 
      // This currentTask hasn't expired, and we've reached the deadline.
      break; // 跳出不执行任务
    }
    const callback = currentTask.callback;
    if (typeof callback === 'function') { // 回调必须是函数
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime; // 任务过期了
      markTaskRun(currentTask, currentTime); // 用于调试
      const continuationCallback = callback(didUserCallbackTimeout); // 执行任务, 任务有一个参数,表示任务是否过期
      currentTime = getCurrentTime(); // 获取当前时间
      if (typeof continuationCallback === 'function') { // 如果任务会返回一个函数
        currentTask.callback = continuationCallback; // 把返回的函数赋值给当前任务的回调
        markTaskYield(currentTask, currentTime); // 用于调试
      } else { // 否则,删除该次任务
        if (enableProfiling) {
          markTaskCompleted(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      advanceTimers(currentTime); // 再次执行: 把不再有延迟的任务(delay时间到了)从 timerQueue 放入到 taskQueue
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue); // 取出下一个任务
  }
  // Return whether there's additional work
  if (currentTask !== null) { // 如果存在下一个任务,结束调用,进入下一次 event loop
    return true;
  } else {
      // 如果不存在下一个任务,并且存在延迟的任务,暂停delay时间后再执行任务
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

最后一个没有仔细讲到的点是任务的排序,我们可以看一下 在 SchedulerMinHeap 文件中定义的几个方法

// 把任务放入到队列中,并执行 siftUp 方法
export function push(heap: Heap, node: Node): void {
  const index = heap.length;
  heap.push(node);
  siftUp(heap, node, index);
}

// 取出第一个任务
export function peek(heap: Heap): Node | null {
  const first = heap[0];
  return first === undefined ? null : first;
}

// 返回第一个任务,把最后一个任务放到第一个位置,调用siftDown
export function pop(heap: Heap): Node | null {
  const first = heap[0];
  if (first !== undefined) {
    const last = heap.pop();
    if (last !== first) {
      heap[0] = last;
      siftDown(heap, last, 0);
    }
    return first;
  } else {
    return null;
  }
}

// siftUp 方法,push 后调用
function siftUp(heap, node, i) {
  let index = i; // i 是 push 后的任务的位置,也就是列表的最后一个位置
  while (true) {
    const parentIndex = (index - 1) >>> 1;// 把位置做位移操作,比如1 >>> 1 = 0, 2 >>> 1 = 1, , 4 >>> 1 = 2
    const parent = heap[parentIndex]; // 获取位移后的位置的任务
    if (parent !== undefined && compare(parent, node) > 0) { // 根据任务的sortIndex比较任务的大小,如果sortIndex相同,就根据任务的id比较;sortIndex就是任务的开始时间(currentTime + delay)。
      // The parent is larger. Swap positions.
      // 如果parent任务的开始时间比较晚,则和当前任务进行交互
      // 其实就是个排序算法,按任务开始时间进行排序
      heap[parentIndex] = node;
      heap[index] = parent;
      index = parentIndex;
    } else {
      // The parent is smaller. Exit.
      return;
    }
  }
}

// siftDown 方法,pop 后调用
// 这个方法一开始很不理解,为什么要把最后一个任务放到第一个任务位置上,再给它排序
// 后来想了想,个人感觉是为了避免优先级较低的任务长时间不执行
function siftDown(heap, node, i) {
  let index = i;
  const length = heap.length;
  while (index < length) {
    const leftIndex = (index + 1) * 2 - 1;
    const left = heap[leftIndex];
    const rightIndex = leftIndex + 1;
    const right = heap[rightIndex];

    // If the left or right node is smaller, swap with the smaller of those.
    if (left !== undefined && compare(left, node) < 0) {
      if (right !== undefined && compare(right, left) < 0) {
        heap[index] = right;
        heap[rightIndex] = node;
        index = rightIndex;
      } else {
        heap[index] = left;
        heap[leftIndex] = node;
        index = leftIndex;
      }
    } else if (right !== undefined && compare(right, node) < 0) {
      heap[index] = right;
      heap[rightIndex] = node;
      index = rightIndex;
    } else {
      // Neither child is smaller. Exit.
      return;
    }
  }
}

function compare(a, b) {
  // Compare sort index first, then task id.
  const diff = a.sortIndex - b.sortIndex;
  return diff !== 0 ? diff : a.id - b.id;
}

(翻译)React hooks:不是魔法,仅仅是数组

原文:https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e

我是 hooks api 的粉丝,但是,在使用 hooks 的时候,它会有一些奇怪的约束。如果你很难理解这些规则,不妨看看这篇文章。

解析 hooks 的工作原理

先让大家能简单的理解新的hooks API的提案。

hooks的2个规则

react 核心小组在提案文档指出,有2个使用规则是开发者必须去遵守的

  • 不要在循环、条件语句、或嵌套函数中调用 hooks
  • hooks 只能在函数组件中使用

第2个规则是很容易理解的,因为 hooks 本来设计的目的就是为了扩展函数式组件。
但是,第1个规则就相对不好理解了,也是这篇文章想去深入探讨的。

hooks 的状态管理用的就是数组

为了更好的理解,我们来看个简单的hooks的实现

注意:这个只是 hooks 的其中一种可能的实现,而不是 hooks 内部真正的实现

怎么实现 useState()

先看个简单的 state hook 的例子

function RenderFunctionComponent() {
  const [firstName, setFirstName] = useState("Rudi");
  const [lastName, setLastName] = useState("Yardley");

  return (
    <Button onClick={() => setFirstName("Fred")}>Fred</Button>
  );
}

你可以让 useState 返回一个 setter 函数,作为返回结果数组的第2个元素,这个 setter 函数会控制这个有 hook 生成的 state。

React是怎么做的

我们先标记下 React 内部可能是怎么实现。在渲染一个组件时会执行下图的逻辑。意思是说,数据是被存储在渲染组建之外。其他组件不共享 state,但是 state 可以响应特定组件随后的渲染。

1) 初始化

创建2个空的数组:setters 和 state
光标指向0
image.png
初始化:2个空的数组,光标是0

2) 首次渲染

第一次只想组件函数
每个 setState 第一次执行,推送一个 setter 函数(绑定一个光标位置)到 setters 数组中,推送一个 state 到 state 数组中
image.png
首次渲染:随着光标增加,各项被写入到数组中

3) 随后的渲染

随后的每次渲染,就是光标的重置,从各个数组中读值
image.png
随后的渲染:随着光标增加,各项从数组中被读取

4) 事件处理

每个 setter 都有一个光标位置的引用,所以每次调用 setter,都会改变对应的 state 的值。
image.png
setters 会记住他们的位置,根据位置去修改存储

简单实现

下面是一个简单的代码示例实现

注意:这并不是 hooks 的完整实现,而是给你一个好的思路去思考 hooks 是怎么工作的。

let state = [];
let setters = [];
let firstRun = true;
let cursor = 0;

function createSetter(cursor) {
  return function setterWithCursor(newVal) {
    state[cursor] = newVal;
  };
}

// This is the pseudocode for the useState helper
export function useState(initVal) {
  if (firstRun) {
    state.push(initVal);
    setters.push(createSetter(cursor));
    firstRun = false;
  }

  const setter = setters[cursor];
  const value = state[cursor];

  cursor++;
  return [value, setter];
}

// Our component code that uses hooks
function RenderFunctionComponent() {
  const [firstName, setFirstName] = useState("Rudi"); // cursor: 0
  const [lastName, setLastName] = useState("Yardley"); // cursor: 1

  return (
    <div>
      <Button onClick={() => setFirstName("Richard")}>Richard</Button>
      <Button onClick={() => setFirstName("Fred")}>Fred</Button>
    </div>
  );
}

// This is sort of simulating Reacts rendering cycle
function MyComponent() {
  cursor = 0; // resetting the cursor
  return <RenderFunctionComponent />; // render
}

console.log(state); // Pre-render: []
MyComponent();
console.log(state); // First-render: ['Rudi', 'Yardley']
MyComponent();
console.log(state); // Subsequent-render: ['Rudi', 'Yardley']

// click the 'Fred' button

console.log(state); // After-click: ['Fred', 'Yardley']

为什么顺序很重要

如果我们改变 hooks 的顺序,当外部因素或组件state变化导致重新渲染时,会发生什么?
让我们试试看

let firstRender = true;

function RenderFunctionComponent() {
  let initName;
  
  if(firstRender){
    [initName] = useState("Rudi");
    firstRender = false;
  }
  const [firstName, setFirstName] = useState(initName);
  const [lastName, setLastName] = useState("Yardley");

  return (
    <Button onClick={() => setFirstName("Fred")}>Fred</Button>
  );
}

破坏了规则

这里我们在一个条件分支中使用了 useState,这导致了很大的问题。

糟糕的首次渲染

image.png
渲染了一个错误的 hook,这个 hook 在后续的渲染会消失

这里我们说明了 firstName 和 lastName 2个变量,数据也是正确的。但是让我们看下第2次渲染

糟糕的第二次渲染
image.png
在渲染的时候去掉了一个 hook,导致了错误

state 存储变得不一致,firstName 和 lastName 都被设置成了 Rudi,这很明显是错误的,但是也让我们明白了 hooks 的规则要这样制定。

不遵守 react team 制定的规则,会导致数据不一致

现在应该明白了为什么 hooks 不能在条件分支和循环中。因为我们处理的是数据集合的光标,要是你改变了调用顺序,光标会对应不上,从而指向错误的数据或处理器。

结论

关于 hooks api 的运行原理,希望我已经讲的比较明白了。最重要的是把这些重要的点组合起来,注意顺序,使用 hooks api 会得到很大的回报。

hooks 是为 react 组件设计的高效的插件式 api。只要你把 state 当成是数组集的模型,你就不会违反他的规则。

一天晚上,我姐给我发了一堆微信,大概意思是给我介绍了个对象,长得挺漂亮,学历很高,工作很好,让我抓紧点,一把年纪了;我敷衍着答应了。

第二天八点多到公司后,用手机号在微信上搜了一下,微信名里有个‘bao’,我想应该就是了,就发送了好友请求。过了一会微信显示好友提过了,我发了句:“hi你好”,“你好”她恢复到。我问:“上班了吗”;“上班了,我们九点上班”她又恢复到。我看已经九点了,就没有再发消息了。

晚上下班到家后,我拿起主机联系了对方,她挺热情友好的介绍了她的工作、学校以及到杭州的经历,过程比较轻松愉快友好;感觉还不错,沟通的挺顺畅的,有个不错的印象。

第二天下班后,我问她:“平常有什么爱好吗?”,过了许久,她回复到:“看书 地球 刷剧 (还有一个忘了,好像是旅行)”。她可能有事在忙,我突然有点语塞,不知如何回复,就匆匆结算了。

过了2天,快到周末了,我想要不约下一起见个面吃个饭。交付交换了一下时间地点,顺利的确定了。

大概提前了十几分钟,我到达了吃饭点滴,找了个角落的位置;过了10几分钟,她匆匆的过来了,穿着羽绒服,个子小小的,披散这头发,摘了口罩后,相视一笑,气氛略显尴尬。幸好服务员来上菜了,边吃边问一些有的没的家长里短的话,气氛稍显缓和。在灯光的映衬下,她带着笑容,说话不紧不慢,语气平缓,如同一位老师再将一个故事娓娓道来,我听得津津有味,不时笑着点头附和。时间流逝,不知不觉8点多了,吃的也差不多了,就一起买单下楼了,我问:“要不要送你回家?”;“不用了,我家离的不远,我坐车几站就到了”,她回复到。第一次见面,也还不是很熟,也不好勉强,不然返回生厌,就没有多说,说了再见后就开车回家了。到家后,礼貌性的问她:“到家没”;“骑车回去的,10几分钟就到了”,她说道。“下次还能再约一起吗”,我又问;过了一会,她回复到“可以啊(带个笑脸)”;我回复以笑脸。

代码优化示例

看了一下公司的项目,从源代码角度看到了一些可优化的部分,有些比较典型,列举如下

1. 缓存数据

/**
 * 获取 browser 信息
 */
export function getBrowser() {
  const ua = window.navigator.userAgent || "";
  const isAndroid = /android/i.test(ua);
  const isIos = /iphone|ipad|ipod/i.test(ua);
  const isWechat = /micromessenger\/([\d.]+)/i.test(ua);
  const isWeibo = /(weibo).*weibo__([\d.]+)/i.test(ua);
  const isQQ = /qq\/([\d.]+)/i.test(ua);
  const isQQBrowser = /(qqbrowser)\/([\d.]+)/i.test(ua);
  const isQzone = /qzone\/.*_qz_([\d.]+)/i.test(ua);
  // 安卓 chrome 浏览器,很多 app 都是在 chrome 的 ua 上进行扩展的
  const isOriginalChrome = /chrome\/[\d.]+ Mobile Safari\/[\d.]+/i.test(ua) && isAndroid;
  // chrome for ios 和 safari 的区别仅仅是将 Version/<VersionNum> 替换成了 CriOS/<ChromeRevision>
  // ios 上很多 app 都包含 safari 标识,但它们都是以自己的 app 标识开头,而不是 Mozilla
  const isSafari =
    /safari\/([\d.]+)$/i.test(ua) &&
    isIos &&
    ua.indexOf("Crios") < 0 &&
    ua.indexOf("Mozilla") === 0;

  return {
    isAndroid,
    isIos,
    isWechat,
    isWeibo,
    isQQ,
    isQQBrowser,
    isQzone,
    isOriginalChrome,
    isSafari
  };
}

getBrowser 方法是用于环境信息的方法,获取的方式本身没什么问题,但是有些信息有个特点:是不可变的,多次调用的结果其实是一样的。
这种情况,我们其实可以把这些环境信息缓存下来,避免重复获取。

const env = (function() {
	return getBrowser();
})()

就是通过自执行函数,预执行 getBrowser 方法,并将结果缓存在 env 变量中,这样就解决了多次调用重复执行的问题。但是这种方式有个问题:会延长加载脚本的时间,而且有可能自始至终都没用到这些信息,这样也是资源的浪费。
我们可以用单例模式的**做进一步优化。

const getEvn = (function() {
	let env;
  return function() {
  	if (!env) {
    	env = getBrowser(); 
    }
    
    return env;
  }
})()

这样的话,只有第一次执行时会调用 getBrowser 方法,后续调用都是用的第一次缓存的结果。上面的代码其实就是典型的单例模式的抽象实现,getBrowser 方法完全可以替换成其他方法。

const getSingle = function(fn) {
    let ret;
  
    return function() {
        if (!ret) {
            result = fn.apply(this, arguments)
        }

        return ret;
    }
}

const getEnv = getSingle(getBrowser);

2. 递归的使用

/**
 * 判断两个版本
 * 比如:'1.5.5','1.5.0'进行比较,返回的是5,前面的版本大于后面5个版本
 * @param {*} preV
 * @param {*} nextV
 */
export const compareVersion = (preV, nextV) => {
  const pvs = preV.split(".");
  const nvs = nextV.split(".");
  const rv = pvs[0] - nvs[0];

  return rv === 0 && preV !== nextV
    ? compareVersion(pvs.splice(1).join("."), nvs.splice(1).join("."))
    : rv;
};

上面的主要是通过递归的方式判断2个版本号的大小。主要有2个问题

  1. 因为在定义 compareVersion 方法是用的字符串作为参数,为了递归调用这个方法,把版本号 split 成数组后,又 join 成了字符串,在每个递归调用中都会重复的 _split、join _。
  2. 很多情况,递归是一种次优的选择,在处理线性的数据结构(数组、队列、单链表等)时,往往是不需要使用递归的,遍历是更优的选择。

第一个问题很容易解决,只需要把递归方法的参数统一用数组即可。

const compareVersion = (preV, nextV) => {
  const pvs = preV.split(".");
  const nvs = nextV.split(".");

  function _compare(pvs, nvs) {
    const rv = pvs[0] - nvs[0];

    return rv === 0 && pvs.length > 1 && nvs.length > 1
    ? _compare(pvs.slice(1), nvs.slice(1))
    : rv;
  }

  return _compare(pvs, nvs);
};

考虑到这个逻辑其实就是2个数组值大小的比对,用遍历可容易实现。

const compareVersion = (preV, nextV) => {
  const pvs = preV.split(".");
  const nvs = nextV.split(".");

  let ret, i = 0;
  for (; i < pvs.length && i < nvs.length; i++) {
    ret = pvs[i] - nvs[i];
    if (ret !== 0) {
      break;
    }
  }
  return ret;
};

另外举个比较典型的遍历 > 递归的例子就是斐波那契数列 F(n) = F(n - 1) + F(n - 2)。如果用递归去解斐波那契数列,会造成大量的计算冗余,性能很低。
当然也可以通过缓存的方式避免重复计算,但是用遍历的方式是更好的选择。

function fib(n) {
  if (n === 1) {
    return 0;
  } 
  
  if (n === 2) {
    return 1;
  }

  let t1 = 0, t2 = 1, i = 3;
  for (; i <= n; i++) {
    if (i === n) {
      return t1 + t2;
    } 

    [t1, t2] = [t2, t1 + t2];
  }
}

3. 代码的组织结构

/**
 * 十进制数字精度转换
 * @param {待转换数字} num
 * @param {保留小数位数}} decimals
 * @param {是否返回“+”符号} withSign
 * 对于null, undefined, NaN均返回0.00
 */
function toFixed(num, decimals = 2, withSign, isOmitZero) {
  let number = num;
  if (typeof decimals !== "number") {
    throw new TypeError("传入toFixed的decimals参数类型不正确");
  }
  if (typeof num !== "number") {
    number = Number(num);
    if (Number.isNaN(number)) {
      return (0).toFixed(decimals);
    }
  }
  if (Number.isNaN(number)) {
    return (0).toFixed(decimals);
  }
  let result = number.toFixed(decimals);
  if (isOmitZero) {
    result = result.replace(/(?:\.0*|(\.\d+?)0+)$/, "$1");
  }
  if (withSign && num > 0) {
    result = `+${result}`;
  }
  return result;
}

当我们要实现一个比较复杂的功能时,我们需要对逻辑进行拆封和排序。以上面的 toFixed 方法为例,它实现的是数据格式化的功能,有4个参数

  1. num 原始数据
  2. decimals 几位小数
  3. withSign 是否带符号
  4. isOmitZero 是否需要将一些 x == 0 的值格式化为0.00

因为不同的数据类型格式化的方式是不同的,所以我们可以按数据的类型将这个功能分为3个部分:

  1. 整数的情况
  2. 小数的情况
  3. x == 0的情况

尽量将特殊情况判断放在前面,这样可以使后面的分支中少很多的特殊情况判断,提取出可抽象的公共的代码逻辑

function toFixed(num, decimals = 2, withSign, isOmitZero) {
  
  function zs(num, decimal) {
    return num + '.' + '0'.repeat(decimal);
  }

  function xs(num, decimal) {
    return parseFloat((num + '0'.repeat(decimal) + '1')).toFixed(decimal);
  }

  function isInt(n){
    return parseInt(n) == parseFloat(n)
  }
  
  function isFloat(n) {
    return parseInt(n) < parseFloat(n)
  }

  let ret = num;
  if (num == 0 && num !== 0 && num !== '0') {
    if (isOmitZero) {
      return '0.00';
    }
  } else if (typeof num === 'string' || typeof num === 'number') {
    if (isInt(num)) {
      ret = zs(num, decimals);
    } else if (isFloat(num)) {
      ret = xs(num, decimals);
    }

    if (withSign && ret > 0) {
      ret = '+' + ret;
    }
  }

  return ret;
}

这样可以让代码的层次更加的清晰、易读。
还可以用策略模式进一步减少分支判断:

const formats = {
  zs: function(num, decimal) {
    return num + '.' + '0'.repeat(decimal);
  },
  xs: function(num, decimal) {
    return parseFloat((num + '0'.repeat(decimal) + '1')).toFixed(decimal);
  },
  default: function(num, decimal) {
    return num;
  },
}

function whichFormat(n){
  const i = parseInt(n);
  const f = parseFloat(n);
  if (i == f) {
    return 'zs';
  } else if (i < f) {
    return 'xs';
  } else {
    return 'default';
  }
}

function toFixed(num, decimals = 2, withSign, isOmitZero) {
  let ret = num;
  if (num == 0 && num !== 0 && num !== '0') {
    if (isOmitZero) {
      return '0.00';
    }
  } else if (typeof num === 'string' || typeof num === 'number') {
    ret = formats[whichFormat(num)](num, decimals);

    if (withSign && ret > 0) {
      ret = '+' + ret;
    }
  }

  return ret;
}

可以看到主要的逻辑实现已经从原方法中进行了拆封,而且该逻辑是可扩展的,原方法中只剩下一些特殊情况的处理逻辑。

数据结构与算法-动态规划与贪心算法

本文基于《javascript数据结构与算法》一书

动态规划

动态规划(Dynamic Programming,DP)是一种将复杂问题分解成更小的子问题来解决的优化技术。用动态规划解决问题时,要遵循三个重要步骤:

  1. 定义子问题;
  2. 实现要反复执行而解决子问题的部分;
  3. 识别并求解出边界条件。

插入排序就是动态规划的**

贪心算法

贪心算法遵循一种近似解决问题的技术,期盼通过每个阶段的局部最优选择(当前最好的 解),从而达到全局的最优(全局最优解)。它不像动态规划那样计算更大的格局。

如何解析运算表达式

vue的template是支持表达式赋值的,比如

{{'hello ' + name}}{{age}}

解析表达式之前,我们先解析出{{}}的内容

let data = {
  abc: 2,
  efg: 3
}

let expr = '{{1 + abc / 2}} - {{efg * 2 }} + \'abc\'';

const exprReg1 = /{{([^{{}}]+)}}/g;

expr.replace(exprReg1, (r, $1) => {
    console.log($1);
})

我们可以看到打印出来的结果

1 + abc / 2
efg * 2 

然后再对上面解析出来的结果做表达式解析

const exprReg2 = /[\s|\+|-|\*|/]?([^\s|\+|-|\*|/]+)[\s|\+|-|\*|/]?/g;

let expr = '1 + abc / 2';

expr.replace(exprReg2, (r, $1) => {
  if (/^[a-zA-Z\$].*/.test($1)) { // 
    console.log($1);
  }
})

查看结果

abc

汇总成一个方法

function renderExpr(expr) {
  function renderParam(expr) {
    return expr.replace(exprReg2, (r, $1) => {
      if (/^[a-zA-Z\$].*/.test($1)) {
        return 'data.' + $1
      }
      return $1;
    })
  } 

  return expr.replace(exprReg1, (r, $1) => {
    return renderParam($1)
  })
}

但是还有个问题,我们每次调用renderExpr方法的时候都会新创建renderParam方法,我们可以用闭包缓存下

function createRenderExpr() {
  function renderParam(expr) {
    return expr.replace(exprReg2, (r, $1) => {
      if (/^[a-zA-Z\$].*/.test($1)) {
        return 'data.' + $1
      }
      return $1;
    })
  }

  return function renderExpr(expr) {
    return expr.replace(exprReg1, (r, $1) => {
      return renderParam($1)
    })
  }
}

const renderExpr = createRenderExpr();

let data = {
  abc: 2,
  efg: 3
}

let expr = '{{ 1 + abc / 2}} - {{efg * 2 }} + \'abc\'';

console.log(renderExpr(expr));

执行结果

1+data.abc/2 - data.efg*2 + 'abc'

vue源码-初始化流程

vue init

从上图的vue初始化完整的流程图中,可以看到vue的一些核心能力,后面会详细解析

  1. observe data
  2. template compile
  3. watch和computed
  4. watcher和nexttick
  5. __patch__和diff
  6. component

(翻译)构建你自己的虚拟DOM

原文:https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060

构建你自己的虚拟DOM,你需要知道2个事情。你不需要理解 React,或者深入其他虚拟DOM的实现源码。他们太过庞大和复杂,事实上,虚拟DOM的核心代码的实现甚至可以少于50行代码。

下面是2个原则:

  • 虚拟DOM是任意一种真实DOM的表现。
  • 当你修改了虚拟DOM树,我们会得到一棵新的树。算法会比较这2棵树,找到差异,对真实DOM做最小的更新。

下面让我们深入上面的2个原则。

表示我们的dom树

首先,我们需要一种方式来存储dom树。我们可以使用 javascript 对象。假设我们有下面这样一颗树:

<ul class=”list”>
  <li>item 1</li>
  <li>item 2</li>
</ul>

看起来是不是很简单,我们如何用 javascript 对象表示这棵树呢?

{
    "type":"ul",
    "props":{
        "class":"list"
    },
    "children":[
        {
            "type":"li",
            "props":{

            },
            "children":[
                "item 1"
            ]
        },
        {
            "type":"li",
            "props":{

            },
            "children":[
                "item 2"
            ]
        }
    ]
}

我们可以注意到2个事情:

  • 我们用对象表示DOM元素。
{ type: ‘…’, props: { … }, children: [ … ] }
  • 我们用字符串表示DOM文本元素。

但是用这种方式编写一棵很大的树会很困难,所我们需要一个帮助方法,这样可以使我们很容易理解DOM树的结构。

function h(type, props, …children) {
  return { type, props, children };
}

现在我们可以这样编写我们的DOM树:

h(‘ul’, { ‘class’: ‘list’ },
  h(‘li’, {}, ‘item 1’),
  h(‘li’, {}, ‘item 2’),
);

看起来清晰多了,但是我们可以更进一步。你应该听说过 JSX ,我想在这样用到它。它是怎么工作的呢?

如果你读了 Babel JSX 文档 ,你会知道 Babel 把下面的代码:

<ul className=”list”>
  <li>item 1</li>
  <li>item 2</li>
</ul>

转化成:

React.createElement(‘ul’, { className: ‘list’ },
  React.createElement(‘li’, {}, ‘item 1’),
  React.createElement(‘li’, {}, ‘item 2’),
);

是不是很相似?如果把 React.createElement(…) 替换为 h(…),我们就可以使用 jsx 语法。我们只需要在文件头部添加一行注释。

/** @jsx h */
<ul className=”list”>
  <li>item 1</li>
  <li>item 2</li>
</ul>

它会告诉 BabelReact.createElement(…) 替换为 h(…)。当然,h 也可以用其他任意的方法。

做个总结,我会这样写 DOM

/** @jsx h */
const a = (
  <ul className=”list”>
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);

Babel 会把它翻译为:

const a = (
  h(‘ul’, { className: ‘list’ },
    h(‘li’, {}, ‘item 1’),
    h(‘li’, {}, ‘item 2’),
  );
);

h 方法执行时,它会返回 js 对象 - 虚拟DOM:

const a = (
  { type: ‘ul’, props: { className: ‘list’ }, children: [
    { type: ‘li’, props: {}, children: [‘item 1’] },
    { type: ‘li’, props: {}, children: [‘item 2’] }
  ] }
);

应用一下 DOM 表达

现在我们有了根据我们的要求用 js 对象表达的 DOM 树。但是我们需要根据 DOM 树创建真实的 DOM。因为我们不能直接追加表达式到真实的DOM中。

首先我们做一些假设,制定一下术语:

  • 真实DOM节点的变量我会以 $ 开头,因此 $parent 就是一个真实的DOM元素

  • 虚拟DOM表达式将在名为node的变量中

  • 比如在 React 中,你只会有一个根节点(one root node),其他所有的节点都在根节点下

入上文所说,让我们创建 createElement(…),函数会把一个虚拟DOM节点转化为一个真实DOM节点。暂时忘记 propschildren,我们会在后面设置。

function createElement(node) {
  if (typeof node === ‘string’) {
    return document.createTextNode(node);
  }
  return document.createElement(node.type);
}

我们支持 文本节点 (就是js字符串) 和 元素节点 (就是js对象)。

{ type: ‘…’, props: { … }, children: [ … ] }

这样,我们可以同时传递文本节点和元素节点。

现在我们考虑一下 childrenchildren 的每个元素要么是文本节点,要么是元素节点。所以我们也可以通过 createElement(…) 方法创建。有没有感觉就是个递归,我们可以未元素 children 的调用 createElement(…) 方法,appendChild() 他们到元素中。

function createElement(node) {
  if (typeof node === ‘string’) {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  node.children
    .map(createElement)
    .forEach($el.appendChild.bind($el));
  return $el;
}

我们暂时把 props 放到一边,后面再讨论。理解虚拟DOM的基础原则不需要用到它们, 而且它们会增加复杂度。

处理更新

现在我们可以把一个虚拟DOM转化为真实的DOM,是时候取考虑比对我们的虚拟DOM树。就是说,我们需要实现一个算法,算法会比对2棵虚拟DOM树 - 新的和旧的,真实的DOM只做必要的更新。

如何比较2棵树?我们需要处理下一个例子:

  • 有些地方没有旧的节点,我们调用 appendChild(…) 添加一个新的节点。
  • 有些地方没有新的节点,我们调用 removeChild(…) 删除这个节点。
  • 如果一个地方有不同的2个节点,我们调用 replaceChild(…) 更新这个节点。
  • 如果节点相同,我们需要更深入,去比较子节点。

让我们创建一个函数 updateElement(…),函数有3个参数-$parent, newNode and oldNode。$parent 是真实的DOM节点,虚拟DOM的父亲节点。现在让我们看看怎么处理上面提到的所有例子。

没有旧的节点

这个很简单,我都不用注释:

function updateElement($parent, newNode, oldNode) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  }
}

没有新的节点

如果新的虚拟DOM树的当前位置没有节点,我们需要从真实DOM中删除对应的节点。我们知道父亲节点,这样就可以调用 *$parent.removeChild(…)*方法,传递真实的DOM节点引用。但是我们没有这个引用。但是如果我们知道节点在夫妻节点中的位置,我们可以通过 $parent.childNodes[index] 拿到这个引用,inex 是节点在父亲节点中的位置。

假设 index 会传递到我们的方法中。代码如下:

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  }
}

节点更新

首先我们创建一个方法用于比较2个节点(新的和旧的),告诉我们哪个节点有变化。我们需要同时考虑元素节点和文本节点。

function changed(node1, node2) {
  return typeof node1 !== typeof node2 ||
         typeof node1 === ‘string’ && node1 !== node2 ||
         node1.type !== node2.type
}

有了 index,我们很容易用新节点替换它:

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  } else if (changed(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    );
  }
}

比对children

最好,我们需要遍历节点的子节点并且进行比对。我们把这个过程叫 *updateElement(…) *。就是个递归。

在写代码之前我们需要考虑到:

  • 只有元素节点需要比对子节点,因为文本节点没有子节点。
  • 我们传递当前节点作为父亲节点。
  • 我们需要一个个比对所有的子节点,即使遇到 undefined,我们的方法也可以处理。
  • index 就是子节点在 children 数组中的索引。
function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  } else if (changed(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    );
  } else if (newNode.type) {
    const newLength = newNode.children.length;
    const oldLength = oldNode.children.length;
    for (let i = 0; i < newLength || i < oldLength; i++) {
      updateElement(
        $parent.childNodes[index],
        newNode.children[i],
        oldNode.children[i],
        i
      );
    }
  }
}

合在一起

/** @jsx h */

function h(type, props, ...children) {
  return { type, props, children };
}

function createElement(node) {
  if (typeof node === 'string') {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  node.children
    .map(createElement)
    .forEach($el.appendChild.bind($el));
  return $el;
}

function changed(node1, node2) {
  return typeof node1 !== typeof node2 ||
         typeof node1 === 'string' && node1 !== node2 ||
         node1.type !== node2.type
}

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  } else if (changed(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    );
  } else if (newNode.type) {
    const newLength = newNode.children.length;
    const oldLength = oldNode.children.length;
    for (let i = 0; i < newLength || i < oldLength; i++) {
      updateElement(
        $parent.childNodes[index],
        newNode.children[i],
        oldNode.children[i],
        i
      );
    }
  }
}

// ---------------------------------------------------------------------

const a = (
  <ul>
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);

const b = (
  <ul>
    <li>item 1</li>
    <li>hello!</li>
  </ul>
);

const $root = document.getElementById('root');
const $reload = document.getElementById('reload');

updateElement($root, a);
$reload.addEventListener('click', () => {
  updateElement($root, b, a);
});

结论

恭喜,我们做到了。我们实现了虚拟DOM。通过这个文章,我们读者能理解虚拟DOM的基本原则,以及 React 底层是怎么工作的。

然而有一些这里没有提到的:

  • 设置元素属性(props),区别和更新它们
  • 事件处理,添加事件到元素中。
  • 让虚拟DOM支持组件,就像 React
  • 从真实DOM中获取引用
  • 引入可以处理真实DOM变化的三方库,比如 jQuery 和它的插件

数据结构与算法-排序

基于《javascript数据结构预算法》一书的5大排序算法

/**
 * 冒泡排序比较任何两个相邻的项,如果第一个比第二个大,则交换它们。元素项向上移动至 正确的顺序,就好像气泡升至表面一样,冒泡排序因此得名
 */
function bubbleSort(arr = []) {
  if (arr.length <= 1) {
    return;
  }
  for(let i = 0; i < arr.length; i ++) {
    for (let j = 0; j < arr.length - i; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }
}

/**
 * 选择排序算法是一种原址比较排序算法。选择排序大致的思路是找到数据结构中的最小值并 2 将其放置在第一位,接着找到第二小的值并将其放在第二位,以此类推。
 */
function selectionSort(arr = []) {
  if (arr.length <= 1) {
    return;
  }
  for (let i = 0; i < arr.length; i++) {
    let min;
    let minIdx;
    for (let j = i; j < arr.length; j++) {
      if (!min || arr[j] < min) {
        minIdx = j;
        min = arr[j];
      }
    }
    if (i !== minIdx) {
      [arr[i], arr[minIdx]] = [arr[minIdx], arr[i]]
    }
  }
}


/**
 * 插入排序是依次将将后面的元素插入到已经排好序的前面的元素中 
 */
function insertionSort(arr = []) {
  if (arr.length <= 1) {
    return;
  }
  for (let i = 1; i < arr.length; i++) {
    for (let j = i; j >= 0; j--) {
      if (arr[j] < arr[j - 1]) {
        [arr[j], arr[j - 1]] = [arr[j - 1], arr[j]];
      } else {
        break;
      }
    }
  }
}


/**
 * 归并排序是一种分治算法。其**是将原始数组切分成较小的数组,直到每个小数组只有一 个位置,接着将小数组归并成较大的数组,直到最后只有一个排序完毕的大数组。
 */
function mergeSort(arr = []) {
  if (arr.length <= 1) {
    return arr;
  }

  function merge(left, right) {
    const ret = [];
    let i = 0, j = 0;
    while (i < left.length && j < right.length) {
      if (left[i] < right[j]) {
        ret.push(left[i ++]);
      } else {
        ret.push(right[j ++]);
      }
    }
    
    while (i < left.length) {
      ret.push(left[i ++]);
    }

    while (j < right.length) {
      ret.push(right[j ++]);
    }

    return ret;
  }

  function sliceArr(arr) {
    if (arr.length <= 1) {
      return arr;
    }
    const midInx = Math.floor(arr.length / 2);
    const left = arr.slice(0, midInx);
    const right = arr.slice(midInx, arr.length);
    return merge(sliceArr(left), sliceArr(right));
  }

  return sliceArr(arr);
}

/**
 * 快速排序也使用分治的方法,将原始数组分 为较小的数组(但它没有像归并排序那样将它们分割开)。
 */
function quickSort(arr = []) {

  function sort(arr = []) {
    if (arr.length <= 1) {
      return arr;
    }

    if (arr.length == 2) {
      if (arr[0] > arr[1]) {
        return [arr[1], arr[0]];
      } else {
        return arr;
      }
    }

    let tmp = arr[0], left = [], right = [], i = 0;
    while (i < arr.length) {
      if (arr[i] < tmp) {
        left.push(arr[i]);
      } else {
        right.push(arr[i]);
      }
      i++;
    }

    return [...sort(left), ...sort(right)];
  }

  return sort(arr);
}

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.