Banana-FE / github-weekly

Learn something from Github ,Go !
MIT License
3 stars 0 forks source link

2020-Nov-Week1 #4

Open webfansplz opened 3 years ago

webfansplz commented 3 years ago

Share your knowledge and repository sources from Github . ♥️

2020/11/02 - 2020/11/06 ~

AzTea commented 3 years ago

Repository (仓库地址):https://github.com/lodash/lodash/tree/npm-packages Gain (收获) : lodash源码学习2(Array Methods)

drop.js:创建一个切片数组,去除array前面的n个元素。(n默认值为1。)

源码学习(baseSlice): >>> 0

/**
 * The base implementation of `_.slice` without an iteratee call guard.
 *
 * @private
 * @param {Array} array The array to slice.
 * @param {number} [start=0] The start position.
 * @param {number} [end=array.length] The end position.
 * @returns {Array} Returns the slice of `array`.
 */
function baseSlice(array, start, end) {
  var index = -1,
      length = array.length;

  if (start < 0) { //负数处理
    start = -start > length ? 0 : (length + start);
  }
  end = end > length ? length : end; //end判断、不能大于数组长度
  if (end < 0) {
    end += length;
  }
  length = start > end ? 0 : ((end - start) >>> 0); //舍去浮点
  start >>>= 0; //舍去浮点

  var result = Array(length);
  while (++index < length) { //遍历,return需要的result
    result[index] = array[index + start];
  }
  return result;
}

concat.js:创建一个新数组,将array与任何数组 或 值连接在一起

/**
 * lodash (Custom Build) <https://lodash.com/>
 * Build: `lodash modularize exports="npm" -o ./`
 * Copyright jQuery Foundation and other contributors <https://jquery.org/>
 * Released under MIT license <https://lodash.com/license>
 * Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
 * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
 */

/** Used as references for various `Number` constants. */
// JS中Number类型的最大值: 2的32次幂指-1
var MAX_SAFE_INTEGER = 9007199254740991;

/** `Object#toString` result references. */
var argsTag = '[object Arguments]',
    funcTag = '[object Function]',
    genTag = '[object GeneratorFunction]';

/** Detect free variable `global` from Node.js. */
var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;

/** Detect free variable `self`. */
var freeSelf = typeof self == 'object' && self && self.Object === Object && self;

/** Used as a reference to the global object. */
// 又是一个this,返回一个window对象(使用Function构造函数创建的函数不会创建对其创建上下文的闭包;他们总是在全局创建。执行时,它们只能访问它们自己的局部变量和全局变量,而不能访问函数构造函数调用的范围。这与使用eval解析函数表达式的代码不同)
var root = freeGlobal || freeSelf || Function('return this')();

/**
 * Appends the elements of `values` to `array`.
 *
 * @private
 * @param {Array} array The array to modify.
 * @param {Array} values The values to append.
 * @returns {Array} Returns `array`.
 */
// 工具方法,接收两个数组,返回一个新的数组
function arrayPush(array, values) {
  var index = -1,
      length = values.length,
      offset = array.length;

  while (++index < length) {
    array[offset + index] = values[index];
  }
  return array;
}

/** Used for built-in method references. */
// 保留引用
var objectProto = Object.prototype;

/** Used to check objects for own properties. */
// 判断是否是该对象上的自建属性
var hasOwnProperty = objectProto.hasOwnProperty;

/**
 * Used to resolve the
 * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
 * of values.
 */
// 保留引用、加快访问速度
var objectToString = objectProto.toString;

/** Built-in value references. */
var Symbol = root.Symbol,
    propertyIsEnumerable = objectProto.propertyIsEnumerable,
    spreadableSymbol = Symbol ? Symbol.isConcatSpreadable : undefined;

/**
 * The base implementation of `_.flatten` with support for restricting flattening.
 *
 * @private
 * @param {Array} array The array to flatten. // 需要被扁平化的数组
 * @param {number} depth The maximum recursion depth. // 最大的递归深度。
 * @param {boolean} [predicate=isFlattenable] The function invoked per iteration. 
 * @param {boolean} [isStrict] Restrict to values that pass `predicate` checks.
 * @param {Array} [result=[]] The initial result value. // 期望放到指定的数组里,不传默认为一个空数组。
 * @returns {Array} Returns the new flattened array. //返回一个新的被扁平化的数组
 */

//传入baseFlatten([[1,2],12],1,)
function baseFlatten(array, depth, predicate, isStrict, result) {
  var index = -1,
      length = array.length; //需要被扁平的长度

  predicate || (predicate = isFlattenable);
  result || (result = []);

  while (++index < length) {
    var value = array[index];
    if (depth > 0 && predicate(value)) {
      if (depth > 1) {  //depth如果 >= 2 会将嵌套更多的数组打平、递归调用传入result
        // Recursively flatten arrays (susceptible to call stack limits).
        baseFlatten(value, depth - 1, predicate, isStrict, result);
      } else {
        arrayPush(result, value);
      }
    } else if (!isStrict) {
      result[result.length] = value;
    }
  }
  return result;
}

/**
 * Copies the values of `source` to `array`.
 *
 * @private
 * @param {Array} source The array to copy values from.
 * @param {Array} [array=[]] The array to copy values to.
 * @returns {Array} Returns `array`.
 */
function copyArray(source, array) {
  var index = -1,
      length = source.length;

  array || (array = Array(length)); //array不存在就创建一个length === source.length的数组
  while (++index < length) {
    array[index] = source[index];
  }
  return array;
}

/**
 * Checks if `value` is a flattenable `arguments` object or array.
 * 判断传入的value是否是一个可以被打平的arguments对象或者数组并返回
 * @private
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is flattenable, else `false`.
 */
function isFlattenable(value) {
  return isArray(value) || isArguments(value) ||
    !!(spreadableSymbol && value && value[spreadableSymbol]);
}

/**
 * Creates a new array concatenating `array` with any additional arrays
 * and/or values.
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Array
 * @param {Array} array The array to concatenate.
 * @param {...*} [values] The values to concatenate.
 * @returns {Array} Returns the new concatenated array.
 * @example
 *
 * var array = [1];
 * var other = _.concat(array, 2, [3], [[4]]);
 *
 * console.log(other);
 * // => [1, 2, 3, [4]]
 *
 * console.log(array);
 * // => [1]
 */
function concat() {
  var length = arguments.length,
      args = Array(length ? length - 1 : 0),
      array = arguments[0],
      index = length;

  while (index--) {
    args[index - 1] = arguments[index];
  }
  return length
    ? arrayPush(isArray(array) ? copyArray(array) : [array], baseFlatten(args, 1))
    : [];
}

/**
 * Checks if `value` is likely an `arguments` object.
 *
 * @static
 * @memberOf _
 * @since 0.1.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is an `arguments` object,
 *  else `false`.
 * @example
 *
 * _.isArguments(function() { return arguments; }());
 * // => true
 *
 * _.isArguments([1, 2, 3]);
 * // => false
 */
function isArguments(value) {
  // Safari 8.1 makes `arguments.callee` enumerable in strict mode.
  return isArrayLikeObject(value) && hasOwnProperty.call(value, 'callee') &&
    (!propertyIsEnumerable.call(value, 'callee') || objectToString.call(value) == argsTag);
}

/**
 * Checks if `value` is classified as an `Array` object.
 *
 * @static
 * @memberOf _
 * @since 0.1.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is an array, else `false`.
 * @example
 *
 * _.isArray([1, 2, 3]);
 * // => true
 *
 * _.isArray(document.body.children);
 * // => false
 *
 * _.isArray('abc');
 * // => false
 *
 * _.isArray(_.noop);
 * // => false
 */
var isArray = Array.isArray;

/**
 * Checks if `value` is array-like. A value is considered array-like if it's
 * not a function and has a `value.length` that's an integer greater than or
 * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`.
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is array-like, else `false`.
 * @example
 *
 * _.isArrayLike([1, 2, 3]);
 * // => true
 *
 * _.isArrayLike(document.body.children);
 * // => true
 *
 * _.isArrayLike('abc');
 * // => true
 *
 * _.isArrayLike(_.noop);
 * // => false
 */
function isArrayLike(value) {
  return value != null && isLength(value.length) && !isFunction(value);
}

/**
 * This method is like `_.isArrayLike` except that it also checks if `value`
 * is an object.
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is an array-like object,
 *  else `false`.
 * @example
 *
 * _.isArrayLikeObject([1, 2, 3]);
 * // => true
 *
 * _.isArrayLikeObject(document.body.children);
 * // => true
 *
 * _.isArrayLikeObject('abc');
 * // => false
 *
 * _.isArrayLikeObject(_.noop);
 * // => false
 */
function isArrayLikeObject(value) {
  return isObjectLike(value) && isArrayLike(value);
}

/**
 * Checks if `value` is classified as a `Function` object.
 *
 * @static
 * @memberOf _
 * @since 0.1.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a function, else `false`.
 * @example
 *
 * _.isFunction(_);
 * // => true
 *
 * _.isFunction(/abc/);
 * // => false
 */
function isFunction(value) {
  // The use of `Object#toString` avoids issues with the `typeof` operator
  // in Safari 8-9 which returns 'object' for typed array and other constructors.
  var tag = isObject(value) ? objectToString.call(value) : '';
  return tag == funcTag || tag == genTag;
}

/**
 * Checks if `value` is a valid array-like length.
 *
 * **Note:** This method is loosely based on
 * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a valid length, else `false`.
 * @example
 *
 * _.isLength(3);
 * // => true
 *
 * _.isLength(Number.MIN_VALUE);
 * // => false
 *
 * _.isLength(Infinity);
 * // => false
 *
 * _.isLength('3');
 * // => false
 */
function isLength(value) {
  return typeof value == 'number' &&
    value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
}

/**
 * Checks if `value` is the
 * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
 * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
 *
 * @static
 * @memberOf _
 * @since 0.1.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is an object, else `false`.
 * @example
 *
 * _.isObject({});
 * // => true
 *
 * _.isObject([1, 2, 3]);
 * // => true
 *
 * _.isObject(_.noop);
 * // => true
 *
 * _.isObject(null);
 * // => false
 */
function isObject(value) {
  var type = typeof value;
  return !!value && (type == 'object' || type == 'function');
}

/**
 * Checks if `value` is object-like. A value is object-like if it's not `null`
 * and has a `typeof` result of "object".
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
 * @example
 *
 * _.isObjectLike({});
 * // => true
 *
 * _.isObjectLike([1, 2, 3]);
 * // => true
 *
 * _.isObjectLike(_.noop);
 * // => false
 *
 * _.isObjectLike(null);
 * // => false
 */
function isObjectLike(value) {
  return !!value && typeof value == 'object';
}

module.exports = concat;
BlingSu commented 3 years ago

Repository (仓库地址):https://github.com/webpack-contrib/less-loader/blob/master/src/index.js Gain (收获) :

  1. 单一原则:每个Loader只做一件事,简单,好维护
  2. 链式调用:webpack按顺序链式调用每个Loader
  3. 统一原则:遵循webpack的规则和结构,输入和输出都是字符串,每个Loader完全独立
  4. 无状态原则:转化不同模块的时候,不在loader中保留状态

同步Loader

loader默认导出一个函数,接受匹配到的文件资源字符串和SourceMap,可以选改文件内容字符串后再返回给下一个loader处理,所以最简单的就是

module.exports = function (source, map) {
  return source
}

这里不能用箭头函数哦!!!loader内部的属性和方法都是需要this调用的,比如 this.cacheable()缓存,this.srouceMap判断是否生成 sourceMap等...

来,我们在项目理创建一个Loader文件,然后新建自己写的style-loader:

// /loader/style-loader.js
function loader(source, map) {
  let style = `
    let style = document.createElement('style')
    style.innerHTML = ${JSON.stringify(source)}
    document.head.appendChild(style)
  `
  return style
}
module.exports = loader

这里的source可以堪称处理后的css字符串,通过style标签把它擦到head中,返回的是一个js代码的字符串,webpack把这玩意儿打包放到模块里面

异步Loader

上面的style-loader是同步操作,我们处理source的时候肯定会遇到异步的,可以通过async/await来做,另外一个方法就是通过loader本身的回调函数callback

// loader/less-loader
const less = require('less')
function loader (source) {
  const callback = this.async()
  less.render(source, function (err, res) {
    let { css } = res
    callback(null, css)
  })
}
module.exports = loader

callback传参大概这样:

callback({
  // 当无法转换原内容时,给 Webpack 返回一个 Error
  error: Error | Null,
  // 转换后的内容
  content: String | Buffer,
  // 转换后的内容得出原内容的Source Map(可选)
  sourceMap?: SourceMap,
  // 原内容生成 AST语法树(可选)
  abstractSyntaxTree?: AST 
})

有时候,除了把原本的内容返回之外,还需要返回原本内容对应的sourcemap,比如我们转换less或者scss代码,以及babel-loader转换es6代码,为了方便调试就会和sourcemap一起返回

// loader/less-loader
const less = require('less')
function loader (source) {
  const callback = this.async()
  less.render(source,{sourceMap: {}}, function (err, res) {
    let { css, map } = res
    callback(null, css, map)
  })
}
module.exports = loader

这样就可以在下一个loader里面呢就可以接受到less-loader返回的sourcemap啦

加载本地Loader

loader文件准备好来,就要加载到webapck中,如果加载第三方的化需要安装后在loader属性中写loader的名称,我们加载本地的化就是要把loader的路径配置上

module.exports = {
  module: {
    rules: [{
      test: /\.less/,
      use: [
        {
          loader: './loader/style-loader.js'
        },
        {
          loader: path.resolve(__dirname, "loader", "less-loader")
        }
      ]
    }]
  }
}

我们可以在loader中配置本地loader的相对路径或者绝对路径来告诉webpack,不过这样看起来特别麻烦,其实这里可以用webpack提供的resolveLoader属性,来告诉webpack去哪里解析本地loader

module.exports = {
  module: {
    rules: [{
      test: /\.less/,
      use: [
        {
          loader: 'style-loader'
        },
        {
          loader: 'less-loader'
        }
      ]
    }]
  },
  resolveLoader:{
    modules: [path.resolve(__dirname, 'loader'),'node_modules']
  }
}

这样webpack就会去loader文件下找,没有才会找node_modules

处理参数

在配置loader的时候,经常会给loader传递参数进行配置,一般是通过options属性来传递的,也有和 url-loader 通过字符串来传参的

{
  test: /\.(jpg|png|gif|bmp|jpeg)$/,
  use: 'url-loader?limt=1024&name=[hash:8].[ext]'
}

webpack也提供了query来获取传参;但是query属性很不稳定,像上面通过字符串来传参,query就返回字符串格式,通过options方式就会返回对象格式,这样不容易处理。所以可以借助官方提供的loader-utils处理

const { getOptions, parseQuery,stringifyRequest } = require('loader-utils')

module.exports = function (source, map) {
  // 获取options参数
  const options = getOptions(this)
  // 解析字符串为对象
  parseQuery('?param1=foo')
  // 将绝对路由转换成相对路径
  // 以便能在require或者import中使用以避免绝对路径
  stringifyRequest(this, 'test/lib/index.js')
}

常用的就是 getOptions 将处理后的参数返回出来,他内部实现逻辑也很简单,根据query属性进行处理,如果字符串就调用parseQuery方法进行解析,源码如下

// loader-utils/lib/getOptions.js
'use strict'
const parseQuery = require('./parseQuery')
function getOptions (loaderContext) {
  const query = loaderContext.query
  if (typeof query === 'string' && query !== '') {
    return parseQuery(loaderContext.query)
  }
  if (!query || typeof query !== 'object') {
    return {}
  }
  return query
}
module.exports = getOptions

获取到参数后, 需要对获取的options参数进行完整的校验,避免有漏掉的参数,如果每个去判断就很蛋疼,所有可以用官方提供的 schema-utils 包来验证

const { getOptions } = require('loader-utils')
const { validate } = require('schema-utils')
const schema = require('./schema.json')
module.exports = function (source, map) {
  const options = getOptions(this)
  const configuration = { name: 'Loader Name' }
  validate(schema, options, configuration)
  // ...
}

validate 函数没有返回值,理由是如果参数不对就会直接抛出异常,进程中断,我们这里用schema.json来表示对options参数进行json格式的对应表

{
  "type": "object",
  "properties": {
    "source": {
      "type": "boolean"
    },
    "name": {
      "type": "string"
    },
  },
  "additionalProperties": false
}

properties中的键名就是我们要校验的options中的字段名称,additionalProperties代表是否允许options中还有其他额外的属性。

less-loader源码分析

上述就是简单的less-loader,看下官方的操作吧?

import less from 'less';
import { getOptions } from 'loader-utils';
import { validate } from 'schema-utils';
import schema from './options.json';
async function lessLoader(source) {
  const options = getOptions(this);
  //校验参数
  validate(schema, options, {
    name: 'Less Loader',
    baseDataPath: 'options',
  });
  const callback = this.async();
  //对options进一步处理,生成less渲染的参数
  const lessOptions = getLessOptions(this, options);
  //是否使用sourceMap,默认取options中的参数
  const useSourceMap =
    typeof options.sourceMap === 'boolean' 
    ? options.sourceMap : this.sourceMap;
  //如果使用sourceMap,就在渲染参数加入
  if (useSourceMap) {
    lessOptions.sourceMap = {
      outputSourceFiles: true,
    };
  }
  let data = source;
  let result;
  try {
    result = await less.render(data, lessOptions);
  } catch (error) {
  }
  const { css, imports } = result;
  //有sourceMap就进行处理
  let map =
    typeof result.map === 'string' 
    ? JSON.parse(result.map) : result.map;

  callback(null, css, map);
}
export default lessLoader;

可以看到官方的less-loader和我们写的简单的loader本质上都是调用less.render函数,对文件资源字符串进行处理,然后将处理好后的字符串和sourceMap通过callback返回。

loader依赖

在loader中,有时候也会用到外部资源,我们需要在loader对这些外部资源进行声明,主要是让他缓存loader失效,这样比较好观察编译。
这里我们写一个假的banner-loader,在每个js文件资源后面加上我们自定义的注释内容,如果传了filename,就从文件中获取已经设定好的banner内容,这里设置两个txt

// loader/banner1.txt
/* build from banner1 */

// loader/banner2.txt
/* build from banner2 */

然后在banner-loader中根据参数来进行判断

// loader/banner-loader
const fs = require('fs')
const path = require('path')
const { getOptions } = require('loader-utils')

module.exports = function (source) {
  const options = getOptions(this)
  if (options.filename) {
    let txt = ''
    if (options.filename == 'banner1') {
      this.addDependency(path.resolve(__dirname, './banner1.txt'))
      txt = fs.readFileSync(path.resolve(__dirname, './banner1.txt'))
    } else if (options.filename == 'banner2') {
      this.addDependency(path.resolve(__dirname, './banner1.txt'))
      txt = fs.readFileSync(path.resolve(__dirname, './banner1.txt'))
    }
    return source + txt
  } else if (options.text) {
    return source + `/* ${options.text} */`
  } else {
    return source
  }
}

这里this.addDependency就是把当前处理的文件添加到依赖中,如果依赖的txt发生来变化,那么打包内容要重新变化。

缓存加速

loader处理需要计算大量的计算,很耗性能,如果每次都重新执行相同的转换操作构建的话就会特别慢,所有webpack默认会把loader处理的记过标记变可缓存,也就是需要被处理的文件或者其依赖的文件没有变化时,输出肯定是相同的,反之不想缓存的话可以这么做

module.exports = function(source) {
  // 强制不缓存
  this.cacheable(false)
  return source
}
scorpioLh commented 3 years ago

Repository (仓库地址):https://github.com/treadpit/wx_calendar/tree/master/src/component/v2 Doc (文档地址):https://treadpit.github.io/wx_calendar/ Gain (收获) : 微信小程序日历组件的学习(V2版本)

alt

calendar(v2)
|--- index.wxml     日历的结构 
|--- index.wxss     日历的样式
|--- index.js       日历的一些基础字段和事件,例如左右滑动,控制参数等
|--- index.json     定义此项目为component
|--- render.js      渲染日历的render函数
|--- core.js        设置日历面板数据等算法
|--- helper.js      日历助手,提供当前时间的上一个月 下一个月 等数据
|--- theme          主题包
|--- utils          工具方法(打印日志等)
|--- plugins        插件包(农历、todo等)

index.wxml 代码结构主要分成头部操作栏、星期栏、日期主体面板三块。日期主体面板分为上月日期、本月日期、下月日期,各由一个for循环构成。

theme

|--- iconfont.wxss         icon
|--- theme-default.wxss    默认主题
|--- theme-elegant.wxss    简介主题

主题包的原理主要还是命名拼接,例如星期栏的颜色显示方式为{{config.theme}}_week-colorconfig.theme源自用户选择的主题名,例如当theme为default时,获得的结果为.default_week-color { color: #ff629a; },如果theme为elegant时,获得的结果是.elegant_week-color { color: #333; }

该项目难点主要集中在日期主体面板的展示,例如,当前月份是从周几开始?这个关系到需要展示几天的上个月日期。还要知道这个月周几结束,关系到需要展示几天的下个月日期。

获取上个月需要展示的日期数组:

  /**
   * 计算指定月份共多少天
   * @param {number} year 年份
   * @param {number} month  月份
   */
  getDatesCountOfMonth(year, month) {
    return new Date(Date.UTC(year, month, 0)).getUTCDate()
  }

  /**
   * 计算指定月份第一天星期几
   * @param {number} year 年份
   * @param {number} month  月份
   */
  firstDayOfWeek(year, month) {
    return new Date(Date.UTC(year, month - 1, 1)).getUTCDay()
  }

  /**
   * 计算指定日期星期几
   * @param {number} year 年份
   * @param {number} month  月份
   * @param {number} date 日期
   */
  getDayOfWeek(year, month, date) {
    return new Date(Date.UTC(year, month - 1, date)).getUTCDay()
  }

  /**
   * 获取上个月的月份信息
   */
getPrevMonthInfo(date = {}) {
    const prevMonthInfo =
      Number(date.month) > 1
        ? {
            year: +date.year,
            month: Number(date.month) - 1
          }
        : {
            year: Number(date.year) - 1,
            month: 12
          }
    return prevMonthInfo
  }

/**
 * 计算上月应占的格子
 * @param {number} year 年份
 * @param {number} month 月份
 */
function calculatePrevMonthGrids(year, month, config) {
  let empytGrids = []
  const prevMonthDays = dateUtil.getDatesCountOfMonth(year, month - 1)
  let firstDayOfWeek = dateUtil.firstDayOfWeek(year, month)
  // 判断用户是否将周一设置为一周的第一天
  if (config.firstDayOfWeek === 'Mon') {
    if (firstDayOfWeek === 0) {
      firstDayOfWeek = 6
    } else {
      firstDayOfWeek -= 1
    }
  }
  if (firstDayOfWeek > 0) {
    const len = prevMonthDays - firstDayOfWeek
    // onlyShowCurrentMonth日历面板是否只显示本月日期
    const { onlyShowCurrentMonth } = config
    const YMInfo = dateUtil.getPrevMonthInfo({ year, month })
    for (let i = prevMonthDays; i > len; i--) {
      if (onlyShowCurrentMonth) { // 插空字符串进去占一个数组元素位置
        empytGrids.push('')
      } else {
        const week = dateUtil.getDayOfWeek(+year, +month, i)
        empytGrids.push({
          ...YMInfo,
          date: i,
          week
        })
      }
    }
    empytGrids.reverse()
  }
  console.log('empytGrids', empytGrids)
  return empytGrids
}

获取下个月需要展示的日期数组:

/**
 * 计算下一月日期是否需要多展示的日期
 * 某些月份日期为5排,某些月份6排,统一为6排
 * @param {number} year
 * @param {number} month
 * @param {object} config
 */
function calculateExtraEmptyDate(year, month, config) {
  let extDate = 0
  if (+month === 2) {
    extDate += 7
    let firstDayofMonth = dateUtil.getDayOfWeek(year, month, 1)
    // 判断用户是否将周一设置为一周的第一天
    if (config.firstDayOfWeek === 'Mon') {
      if (+firstDayofMonth === 1) extDate += 7
    } else {
      if (+firstDayofMonth === 0) extDate += 7
    }
  } else {
    let firstDayofMonth = dateUtil.getDayOfWeek(year, month, 1)
    // 判断用户是否将周一设置为一周的第一天
    if (config.firstDayOfWeek === 'Mon') {
      if (firstDayofMonth !== 0 && firstDayofMonth < 6) {
        extDate += 7
      }
    } else {
      if (firstDayofMonth <= 5) {
        extDate += 7
      }
    }
  }
  return extDate
}
/**
 * 计算下月应占的格子
 * @param {number} year 年份
 * @param {number} month  月份
 */
function calculateNextMonthGrids(year, month, config) {
  let emptyGrids = []
  const datesCount = dateUtil.getDatesCountOfMonth(year, month)
  let lastDayWeek = dateUtil.getDayOfWeek(year, month, datesCount)
  if (config.firstDayOfWeek === 'Mon') {
    if (lastDayWeek === 0) {
      lastDayWeek = 6
    } else {
      lastDayWeek -= 1
    }
  }
  let len = 7 - (lastDayWeek + 1)
  // onlyShowCurrentMonth日历面板是否只显示本月日期
  const { onlyShowCurrentMonth } = config
  if (!onlyShowCurrentMonth) {
    len = len + calculateExtraEmptyDate(year, month, config)
  }
  const YMInfo = dateUtil.getNextMonthInfo({ year, month })
  for (let i = 1; i <= len; i++) {
    const week = dateUtil.getDayOfWeek(+year, +month, i)
    if (onlyShowCurrentMonth) { // 插空字符串进去占一个数组元素位置
      emptyGrids.push('')
    } else {
      emptyGrids.push({
        id: i - 1,
        ...YMInfo,
        date: i,
        week: week || 7
      })
    }
  }
  console.log('下个月:', emptyGrids)
  return emptyGrids
}

获取当前月需要展示的日期数组:

  /**
   * 获取今天的日期
   */
  todayFMD() {
    const _date = new Date()
    const year = _date.getFullYear()
    const month = _date.getMonth() + 1
    const date = _date.getDate()
    return {
      year: +year,
      month: +month,
      date: +date
    }
  }

  /**
   * 组合当前月份数组
   */
  calcDates(year, month) {
    const datesCount = this.getDatesCountOfMonth(year, month)
    const dates = []
    const today = dateUtil.todayFMD()
    for (let i = 1; i <= datesCount; i++) {
      const week = dateUtil.getDayOfWeek(+year, +month, i)
      // isToday 给今天做标记
      const date = {
        year: +year,
        id: i - 1,
        month: +month,
        date: i,
        week,
        isToday:
          +today.year === +year && +today.month === +month && i === +today.date
      }
      dates.push(date)
    }
    return dates
  }

/**
 * 设置当前月数据
 * @param {number} year 年份
 * @param {number} month  月份
 * @param {number} curDate  日期
 */
function calculateCurrentMonthDates(year, month) {
  return dateUtil.calcDates(year, month)
}

将上个月显示的日期 + 当前月日期 + 下个月显示的日期组合起来,形成日历面板:

/**
 * 计算当前月份前后两月应占的格子
 * @param {number} year 年份
 * @param {number} month 月份
 */
function calculateEmptyGrids(year, month, config) {
  const prevMonthGrids = calculatePrevMonthGrids(year, month, config)
  const nextMonthGrids = calculateNextMonthGrids(year, month, config)
  return {
    prevMonthGrids,
    nextMonthGrids
  }
}

/**
 * 组合当前面板显示数据
 */
export function calcJumpData({ dateInfo, config, component }) {
  dateInfo = dateInfo || dateUtil.todayFMD()
  const { year, month, date } = dateInfo
  const calendarConfig = config || getCalendarConfig(component)
  const emptyGrids = calculateEmptyGrids(year, month, calendarConfig)
  const calendar = {
    curYear: year,
    curMonth: month,
    curDate: date,
    dates: calculateCurrentMonthDates(year, month),
    ...emptyGrids
  }
  console.log('calendar:', calendar)
  return calendar
}

最后得到的数据格式:

curDate: 4
curMonth: 11
curYear: 2020
dates: (30) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
nextMonthGrids: (6) [{…}, {…}, {…}, {…}, {…}, {…}]
prevMonthGrids: (6) [{…}, {…}, {…}, {…}, {…}, {…}]
selectedDates: [{…}]

总结:主要了解的是源码作者如何处理日历面板展示的思路,主要将日期面板分为“上个月最后几天” + “当前整个月” + “下个月前几天”,分别由3个数组显示。这些并不是该源码全部,还有其他功能,例如农历、TODO等,还在学习中。。。

wenfeihuazha commented 3 years ago

Repository (仓库地址):https://github.com/snabbdom/snabbdom Gain (收获) : snabbdom

关于snabbdom

其实snabbdom就是我们常说的虚拟DOM的实现,在vue、react等前端常用框架中也大量借鉴了snabbdom的写法去实现虚拟DOM转换为真实DOM

核心思路,通过对比两个虚拟DOM,diff算法替换修改的dom,再通过源码中的DOMAPI实现将虚拟DOM转换为DOM

关于对比两个虚拟DOM的部分核心代码


// 主要对比function
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
  // 因为 vnode 和 oldVnode 是相同的 vnode,所以我们可以复用 oldVnode.elm。
  const elm = vnode.elm = oldVnode.elm
  let oldCh = oldVnode.children
  let ch = vnode.children

  // 如果 oldVnode 和 vnode 是完全相同,说明无需更新,直接返回。
  if (oldVnode === vnode) return

  // 调用 update hook
  if (vnode.data) {
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
  }

  // 判断是否为文本节点,在snabbdom中text文本节点和children子节点并不共存
  if (vnode.text === undefined) {
    // 比较 old children 和 new children,并更新
    if (oldCh && ch) {
      if (oldCh !== ch) {
        // 核心逻辑(最复杂的地方):怎么比较新旧 children 并更新,对应上面的数组比较
        updateChildren(elm, oldCh, ch, insertedVnodeQueue)
      }
    }
    // 添加新 children
    else if (ch) {
      // 首先删除原来的 text
      if (oldVnode.text) api.setTextContent(elm, '')
      // 然后添加新 dom(对 ch 中每个 vnode 递归创建 dom 并插入到 elm)
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    }
    // 相反地,如果原来有 children 而现在没有,那么我们要删除 children。
    else if (oldCh) {
      removeVnodes(elm, oldCh, 0, oldCh.length - 1);
    }
    // 最后,如果 oldVnode 有 text,删除。
    else if (oldVnode.text) {
      api.setTextContent(elm, '');
    }
  }
  // 否则 (vnode 有 text),只要 text 不等,更新 dom 的 text。
  else if (oldVnode.text !== vnode.text) {
    api.setTextContent(elm, vnode.text)
  }
}

function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
  let oldStartIdx = 0, newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx
  let idxInOld
  let elmToMove
  let before

  // 遍历 oldCh 和 newCh 来比较和更新(也就是关于对比虚拟DOM的diff实现)
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 一 首先检查 4 种情况,保证 oldStart/oldEnd/newStart/newEnd
    // 这 4 个 vnode 非空,左侧的 vnode 为空就右移下标,右侧的 vnode 为空就左移 下标。
    if (oldStartVnode == null) {
      oldStartVnode = oldCh[++oldStartIdx]
    } else if (oldEndVnode == null) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (newStartVnode == null) {
      newStartVnode = newCh[++newStartIdx]
    } else if (newEndVnode == null) {
      newEndVnode = newCh[--newEndIdx]
    }
    /**
     * 二 然后 oldStartVnode/oldEndVnode/newStartVnode/newEndVnode 两两比较,
     * 对有相同 vnode 的 4 种情况执行对应的 patch 逻辑。
     * - 如果同 start 或同 end 的两个 vnode 是相同的(情况 1 和 2),
     *   说明不用移动实际 dom,直接更新 dom 属性/children 即可;
     * - 如果 start 和 end 两个 vnode 相同(情况 3 和 4),
     *   那说明发生了 vnode 的移动,同理我们也要移动 dom。
     */
    // 1. 如果 oldStartVnode 和 newStartVnode 相同(key相同),执行 patch
    else if (isSameVnode(oldStartVnode, newStartVnode)) {
      // 不需要移动 dom
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    }
    // 2. 如果 oldEndVnode 和 newEndVnode 相同,执行 patch
    else if (isSameVnode(oldEndVnode, newEndVnode)) {
      // 不需要移动 dom
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    }
    // 3. 如果 oldStartVnode 和 newEndVnode 相同,执行 patch
    else if (isSameVnode(oldStartVnode, newEndVnode)) {
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
      // 把获得更新后的 (oldStartVnode/newEndVnode) 的 dom 右移,移动到
      // oldEndVnode 对应的 dom 的右边。为什么这么右移?
      // (1)oldStartVnode 和 newEndVnode 相同,显然是 vnode 右移了。
      // (2)若 while 循环刚开始,那移到 oldEndVnode.elm 右边就是最右边,是合理的;
      // (3)若循环不是刚开始,因为比较过程是两头向中间,那么两头的 dom 的位置已经是
      //     合理的了,移动到 oldEndVnode.elm 右边是正确的位置;
      // (4)记住,oldVnode 和 vnode 是相同的才 patch,且 oldVnode 自己对应的 dom
      //     总是已经存在的,vnode 的 dom 是不存在的,直接复用 oldVnode 对应的 dom。
      api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm))
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    }
    // 4. 如果 oldEndVnode 和 newStartVnode 相同,执行 patch
    else if (isSameVnode(oldEndVnode, newStartVnode)) {
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
      // 这里是左移更新后的 dom,原因参考上面的右移。
      api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    }

    // 三 最后一种情况:4 个 vnode 都不相同,那么我们就要
    // 1. 从 oldCh 数组建立 key --> index 的 map。
    // 2. 只处理 newStartVnode (简化逻辑,有循环我们最终还是会处理到所有 vnode),
    //    以它的 key 从上面的 map 里拿到 index;
    // 3. 如果 index 存在,那么说明有对应的 old vnode,patch 就好了;
    // 4. 如果 index 不存在,那么说明 newStartVnode 是全新的 vnode,直接
    //    创建对应的 dom 并插入。
    else {
      // 如果 oldKeyToIdx 不存在,创建 old children 中 vnode 的 key 到 index 的
      // 映射,方便我们之后通过 key 去拿下标。
      if (oldKeyToIdx === undefined) {
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      }
      // 尝试通过 newStartVnode 的 key 去拿下标
      idxInOld = oldKeyToIdx[newStartVnode.key]
      // 下标不存在,说明 newStartVnode 是全新的 vnode。
      if (idxInOld == null) {
        // 那么为 newStartVnode 创建 dom 并插入到 oldStartVnode.elm 的前面。
        api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm)
        newStartVnode = newCh[++newStartIdx]
      }
      // 下标存在,说明 old children 中有相同 key 的 vnode,
      else {
        elmToMove = oldCh[idxInOld]
        // 如果 type 不同,没办法,只能创建新 dom;
        if (elmToMove.type !== newStartVnode.type) {
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm)
        }
        // type 相同(且key相同),那么说明是相同的 vnode,执行 patch。
        else {
          patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
          oldCh[idxInOld] = undefined
          api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm)
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
  }

  // 上面的循环结束后(循环条件有两个),处理可能的未处理到的 vnode。
  // 如果是 new vnodes 里有未处理的(oldStartIdx > oldEndIdx
  // 说明 old vnodes 先处理完毕)
  if (oldStartIdx > oldEndIdx) {
    before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
    addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  }
  // 相反,如果 old vnodes 有未处理的,删除 (为处理 vnodes 对应的) 多余的 dom。
  else if (newStartIdx > newEndIdx) {
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  }
}

总结:了解具体的虚拟DOM的实现方式 对我们阅读Vue/React源码会起到巨大的帮助,关于虚拟DOM还有许多细节,可以看源码学习

rxyshww commented 3 years ago

Repository (仓库地址):https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/renderer.ts Gain (收获) : vue渲染流程,diff过程

我们先来简单的看下vue的初始化流程:

1、初始化

import { createApp } from 'vue'
import App from './app'
const app = createApp(App)
app.mount('#app')

export const createApp = ((...args) => {
  // 为了tree-shaking 
  // 如果你不使用vue全部,而只是引用了reactive响应式包做些事情
  // 那么这段代码不运行,内部依赖的代码也不会被打包
  const app = ensureRenderer().createApp(...args)

  const { mount } = app
  // 重写mount方法
  // 重写是因为浏览器,服务端,weex等存在差异,需要修改
  app.mount = (containerOrSelector: Element | string): any => {
    // 标准化容器 就是你传 #app 还是id为app的dom,最后都返回app的dom,作为最终挂载的容器
    const container = normalizeContainer(containerOrSelector)
    // ...
    const proxy = mount(container)
    return proxy
  }

  return app
})

上面的createApp(createAppAPI)大概实现如下

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    let isMounted = false
    const app: App = (context.app = {
      mount(rootContainer: HostElement, isHydrate?: boolean): any {
        if (!isMounted) {
          // 创建根组件的 vnode
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )
          // 利用渲染器渲染 vnode
          render(vnode, rootContainer)
          isMounted = true
          app._container = rootContainer
          // for devtools and telemetry
          ;(rootContainer as any).__vue_app__ = app
          return vnode.component!.proxy
        }
      }
    })

    return app
  }
}

render代码如下:

const render: RootRenderFunction = (vnode, container) => {
    if (vnode == null) {
      // 如果有缓存节点,销毁组件
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      // 创建或者更新组件
      patch(container._vnode || null, vnode, container)
    }
    // 缓存 vnode 节点,表示已经渲染
    container._vnode = vnode
  }

可以看出,初始化中app.mount -> createApp中重写的mount -> createAppAPI中的mount -> mount中的render 上边的代码流程应该还算挺清晰的,如果不能理解也没关系,因为并不影响这次的主题,你只需要清楚这个流程就行

而patch函数,则是整个vue渲染的核心,核心的diff算法,也在其中

先看看这个方法isSameVNodeType, 就是判断两个节点是不是同一个节点,是则可以复用,避免多余的dom操作,不是则直接移除这个节点和他的儿子节点,他会在patch中用到

function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  if (
    __DEV__ &&
    n2.shapeFlag & ShapeFlags.COMPONENT &&
    hmrDirtyComponents.has(n2.type as ConcreteComponent)
  ) {
    // HMR only: if the component has been hot-updated, force a reload.
    return false
  }
  return n1.type === n2.type && n1.key === n2.key
}

再看看patch函数,他会通过初始化vnode时,每个vnode标记的shapeFlag,做不同的处理,这里主要分析组件和dom,主要用到四个参数n1(要被替换的旧节点), n2(新节点), container(他们的霸霸), anchor(参照节点),如果为null,则直接插到container的最后,如果有,则插到anchor之前,以下函数用到的这几个参数作用均相同

// Note: functions inside this closure should use `const xxx = () => {}`
  // style in order to prevent being inlined by minifiers.
  const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    // 有些参数暂不需要,直接省略
  ) => {
    // patching & not same type, unmount old tree
    // 节点不同,删除老节点树
    if (n1 && !isSameVNodeType(n1, n2)) {
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }

    const { type, ref, shapeFlag } = n2
    switch (type) {
      case Text:
        // 处理文本
      case Comment:
        // 处理注释节点
      case Static:
        // 处理被标记的静态节点,
      case Fragment:
        // 处理Fragment元素
      default:
        // 处理原生dom,组件最后还是会转成真实dom
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            // 。。。
          )
        // 处理组件
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          processComponent(
            n1,
            n2,
            container,
            anchor,
            // 。。。
          )
          // 处理 TELEPORT
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          // 处理 SUSPENSE
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
        }
    }
  }

  const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    // 。。。
  ) => {
    // n1是null 那就意味着
    if (n1 == null) {
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        // 。。。
      } else {
        mountComponent(
          n2,
          container,
          anchor,
          // 。。。
        )
      }
    } else {
      updateComponent(n1, n2, optimized)
    }
  }

  const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))
    // 。。。

    // 初始化组件的副作用,会定义一个update方法,类似于一个勾子函数,当数据变化时,响应式数据会派发更新,通知依赖
    // 这个数据的组件,调用update方法
    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )
  }

  const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // create reactive effect for rendering
    instance.update = effect(function componentEffect() {
        let { next, vnode } = instance

        // next 表示新的组件 vnode
        if (next) {
        // 更新组件 vnode 节点信息
            updateComponentPreRender(instance, next, optimized)
        }
        // 渲染新的子树 vnode
        const nextTree = renderComponentRoot(instance)
        // 缓存旧的子树 vnode
        const prevTree = instance.subTree
        // 更新子树 vnode
        instance.subTree = nextTree
        // 组件更新核心逻辑,根据新旧子树 vnode 做 patch
        patch(
            prevTree, 
            nextTree,
            // 如果在 teleport 组件中父节点可能已经改变,所以容器直接找旧树 DOM 元素的父节点
            hostParentNode(prevTree.el),
            // 参考节点在 fragment 的情况可能改变,所以直接找旧树 DOM 元素的下一个节点
            getNextHostNode(prevTree),
            instance,
            parentSuspense,
            isSVG
        )
      // 缓存更新后的 DOM 节点
      next.el = nextTree.el
      }
    })
  }

流程就是通过patch初始化组件时,调用了setupRenderEffect方法,留了个update勾子,当数据更新时,update触发,重新调用patch更新变化后的dom

在patch时,涉及一个更新dom的策略问题,为了方便大家熟悉流程,我画了一张图: WechatIMG1325

可以看出,大致流程就是节点类型不一样时,删除节点,加载新节点,类型一样就更新,然后判断是不是数组,数组的流程会比较复杂,因为直接将数组替换,是个非常消耗性能的方法。所以需要一种策略去优化,源码方法叫patchKeyedChildren,整体放在文章最后,这里我们只分析核心部分。

我们举个例子: 旧列表 abcdef 新列表 abedf

可以看到,新的列表中有些不需要大更新 旧列表 ab [cde] f 新列表 ab [ed] f 我们从左往右循环,ab都相同,直接跳过,到e时停止,再从右往左循环,f相同,到d停止 可以看到,我们需要大更新 [ed],其他的只需要调用patch更新下props啥的

代码实现:


let i = 0
const l2 = c2.length
let e1 = c1.length - 1 // prev ending index
let e2 = l2 - 1 // next ending index

// 1. sync from start
// (a b) c
// (a b) d e
while (i <= e1 && i <= e2) {
    const n1 = c1[i]
    const n2 = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
    if (isSameVNodeType(n1, n2)) {
        patch(
            n1,
            n2,
            container,
            null
        )
    } else {
        break
    }
    i++
}

// 2. sync from end
// a (b c)
// d e (b c)
while (i <= e1 && i <= e2) {
    const n1 = c1[e1]
    const n2 = (c2[e2] = optimized
        ? cloneIfMounted(c2[e2] as VNode)
        : normalizeVNode(c2[e2]))
    if (isSameVNodeType(n1, n2)) {
        patch(
            n1,
            n2,
            container,
            nul
        )
    } else {
        break
    }
    e1--
    e2--
}

源码注释中给了很好的例子

// 3. common sequence + mount
// (a b)
// (a b) c
// 循环后得出 i = 2, e1 = 1, e2 = 2
// (a b)
// c (a b)
// 循环后得出 i = 0, e1 = -1, e2 = 0

他们都有一个共同点 i > e1, 说明元素有增加,而i到e2,就是所增加的元素

if (i > e1) {
    if (i <= e2) {
        // 上文章提到anchor为参照物,具体解释可以搜一下上边
        const nextPos = e2 + 1
        const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
        while (i <= e2) {
            patch(
                null,
                (c2[i] = optimized
                    ? cloneIfMounted(c2[i] as VNode)
                    : normalizeVNode(c2[i])),
                container,
                anchor,
                parentComponent,
                parentSuspense,
                isSVG
            )
            i++
        }
    }
}

再看另一种情况

// 4. common sequence + unmount
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1

我们又发现i > e2 说明需要删除元素,对应删除旧数组中 i -> e1 索引的元素

else if (i > e2) {
    while (i <= e1) {
        unmount(c1[i], parentComponent, parentSuspense, true)
        i++
    }
}

例子 旧列表 ab [cde] f 新列表 ab [ed] fg 走增加的过程 旧列表 ab [cde] fg 新列表 ab [ed] f 走移除的过程

那中间的元素呢,要全部都重新替换吗,虽然可以,但显然还有优化的空间,vue中使用了一个叫最长递增子序列的算法,实现是循环+二分法,经过几次的优化,将时间复杂度复杂度从O(n^2)降到了O(nlogn)。有时间下次再分析。

我们来看看源码中时怎么更新数组的

const patchKeyedChildren = (
    c1: VNode[],
    c2: VNodeArrayChildren,
    container: RendererElement,
    parentAnchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    let i = 0
    const l2 = c2.length
    let e1 = c1.length - 1 // prev ending index
    let e2 = l2 - 1 // next ending index

    // 1. sync from start
    // (a b) c
    // (a b) d e
    while (i <= e1 && i <= e2) {
      const n1 = c1[i]
      const n2 = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
      if (isSameVNodeType(n1, n2)) {
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      } else {
        break
      }
      i++
    }

    // 2. sync from end
    // a (b c)
    // d e (b c)
    while (i <= e1 && i <= e2) {
      const n1 = c1[e1]
      const n2 = (c2[e2] = optimized
        ? cloneIfMounted(c2[e2] as VNode)
        : normalizeVNode(c2[e2]))
      if (isSameVNodeType(n1, n2)) {
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      } else {
        break
      }
      e1--
      e2--
    }

    // 3. common sequence + mount
    // (a b)
    // (a b) c
    // i = 2, e1 = 1, e2 = 2
    // (a b)
    // c (a b)
    // i = 0, e1 = -1, e2 = 0
    if (i > e1) {
      if (i <= e2) {
        const nextPos = e2 + 1
        const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
        while (i <= e2) {
          patch(
            null,
            (c2[i] = optimized
              ? cloneIfMounted(c2[i] as VNode)
              : normalizeVNode(c2[i])),
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG
          )
          i++
        }
      }
    }

    // 4. common sequence + unmount
    // (a b) c
    // (a b)
    // i = 2, e1 = 2, e2 = 1
    // a (b c)
    // (b c)
    // i = 0, e1 = 0, e2 = -1
    else if (i > e2) {
      while (i <= e1) {
        unmount(c1[i], parentComponent, parentSuspense, true)
        i++
      }
    }

    // 5. unknown sequence
    // [i ... e1 + 1]: a b [c d e] f g
    // [i ... e2 + 1]: a b [e d c h] f g
    // i = 2, e1 = 4, e2 = 5
    else {
      const s1 = i // prev starting index
      const s2 = i // next starting index

      // 5.1 build key:index map for newChildren
      const keyToNewIndexMap: Map<string | number, number> = new Map()
      for (i = s2; i <= e2; i++) {
        const nextChild = (c2[i] = optimized
          ? cloneIfMounted(c2[i] as VNode)
          : normalizeVNode(c2[i]))
        if (nextChild.key != null) {
          if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
            warn(
              `Duplicate keys found during update:`,
              JSON.stringify(nextChild.key),
              `Make sure keys are unique.`
            )
          }
          keyToNewIndexMap.set(nextChild.key, i)
        }
      }

      // 5.2 loop through old children left to be patched and try to patch
      // matching nodes & remove nodes that are no longer present
      let j
      let patched = 0
      const toBePatched = e2 - s2 + 1
      let moved = false
      // used to track whether any node has moved
      let maxNewIndexSoFar = 0
      // works as Map<newIndex, oldIndex>
      // Note that oldIndex is offset by +1
      // and oldIndex = 0 is a special value indicating the new node has
      // no corresponding old node.
      // used for determining longest stable subsequence
      const newIndexToOldIndexMap = new Array(toBePatched)
      for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0

      for (i = s1; i <= e1; i++) {
        const prevChild = c1[i]
        if (patched >= toBePatched) {
          // all new children have been patched so this can only be a removal
          unmount(prevChild, parentComponent, parentSuspense, true)
          continue
        }
        let newIndex
        if (prevChild.key != null) {
          newIndex = keyToNewIndexMap.get(prevChild.key)
        } else {
          // key-less node, try to locate a key-less node of the same type
          for (j = s2; j <= e2; j++) {
            if (
              newIndexToOldIndexMap[j - s2] === 0 &&
              isSameVNodeType(prevChild, c2[j] as VNode)
            ) {
              newIndex = j
              break
            }
          }
        }
        if (newIndex === undefined) {
          unmount(prevChild, parentComponent, parentSuspense, true)
        } else {
          newIndexToOldIndexMap[newIndex - s2] = i + 1
          if (newIndex >= maxNewIndexSoFar) {
            maxNewIndexSoFar = newIndex
          } else {
            moved = true
          }
          patch(
            prevChild,
            c2[newIndex] as VNode,
            container,
            null,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
          patched++
        }
      }

      // 5.3 move and mount
      // generate longest stable subsequence only when nodes have moved
      const increasingNewIndexSequence = moved
        ? getSequence(newIndexToOldIndexMap)
        : EMPTY_ARR
      j = increasingNewIndexSequence.length - 1
      // looping backwards so that we can use last patched node as anchor
      for (i = toBePatched - 1; i >= 0; i--) {
        const nextIndex = s2 + i
        const nextChild = c2[nextIndex] as VNode
        const anchor =
          nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
        if (newIndexToOldIndexMap[i] === 0) {
          // mount new
          patch(
            null,
            nextChild,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG
          )
        } else if (moved) {
          // move if:
          // There is no stable subsequence (e.g. a reverse)
          // OR current node is not among the stable sequence
          if (j < 0 || i !== increasingNewIndexSequence[j]) {
            move(nextChild, container, anchor, MoveType.REORDER)
          } else {
            j--
          }
        }
      }
    }
  }
diandiantong commented 3 years ago

小程序实现 table 表格合并:https://github.com/cachecats/wechat-table 收获:复杂的页面布局结构,可通过构建合理的数据结构配合 css 来实现需要的效果。 难点:此组件如需使用动态表格数据,有些数据因为嵌套层级较深,自己计算较为麻烦 div 布局 table 再表格合并上一直是比较麻烦的,如果从数据结构入手便简单多了。上图

首先一份如下结构数据:

[{
      id: 'table001',
      name: '基础工资',
      value: null,
      children: [{
          id: 'table0011',
          name: '基本工资',
          value: 3000.0,
          children: []
        },
        {
          id: 'table0012',
          name: '绩效工资',
          value: 1200.0,
          children: []
        },
        {
          id: 'table0013',
          name: '基本工作量',
          value: null,
          children: [{
              id: 'table00131',
              name: '课时工资',
              value: 800.0,
              children: []
            },
            {
              id: 'table00132',
              name: '超课时工资',
              value: 200.0,
              children: []
            },
          ]
        },
      ]
    },
    {
      id: 'table002',
      name: '加班工资',
      value: null,
      children: [{
          id: 'table0021',
          name: '工作日加班',
          value: 1000.0,
          children: []
        },
        {
          id: 'table0022',
          name: '周末加班',
          value: 600.0,
          children: []
        },
      ]
    },
    {
      id: 'table003',
      name: '岗位工资',
      value: 1800.0,
      children: [
      ]
    },
    {
      id: 'table004',
      name: '合计',
      value: 8600.0,
      children: []
    },
  ]

从数据结构来看,表格的拆分,以行为形式,合并的一个大行作为一个数据块,行内根据不同数据结构合并拆分为各个小行

数据结构完善了,现在配上元素结构

<view class='table-wrapper'>
    /* 暂无提示 */
    <view class='nodata' wx:if='{{list.length === 0}}'>本月暂无工资数据</view>
     /* 行开始 */
    <view class='row1' wx:if='{{list.length > 0}}' wx:for='{{list}}' wx:key='{{item.id}}'>
       /* 每行单元格里面显示的数据 */
      <text class='text'>{{item.name}}</text>
       /* 重复开一个大行(这里其实是套娃模式,想要无限套娃,直接把大块作为组件,无限调用做套娃模式就好了) */
      <view class='column2-wrapper'>
        <view class='column-value' wx:if='{{item.value}}'>{{item.value}}</view>
        <view class='column2' wx:if='{{item.children.length > 0}}' wx:for='{{item.children}}' wx:for-item='item2' wx:key='{{item2.id}}'>
          <text class='text'>{{item2.name}}</text>
          <view class='column3-wrapper'>
            <view class='column-value' wx:if='{{item2.value}}'>{{item2.value}}</view>
            <view class='column3' wx:if='{{item2.children.length > 0}}' wx:for='{{item2.children}}' wx:for-item='item3' wx:key='{{item3.id}}'>
              <text class='text'>{{item3.name}}</text>
              <view class='column4-wrapper'>
                <view class='column-value' wx:if='{{item3.value}}'>{{item3.value}}</view>
              </view>
            </view>
          </view>
        </view>
      </view>
    </view>
  </view>

比较重要的 css 设置

.table-wrapper {
  width: 100%;
  display: flex;    
  flex-direction: column; /* 排列方式 */
  border-top: 1rpx solid #DCDFE6;
}
.row1 {
  width: 100%;
  display: flex;
  flex-direction: row;
  align-items: center;  /* 居中设置 */
  font-size: 32rpx;
  box-sizing: border-box;
  border-bottom: 1rpx solid #DCDFE6;
  
}
.text {
  flex: 1;
  padding: 10rpx;
  line-height: 60rpx;
  height: 60rpx;
}
/* 子数据的排列方式 */
.column2-wrapper {
  display: flex;
  flex-direction: column;
  flex: 3;
  justify-content: center;
  border-left: 1rpx solid #DCDFE6;
}
.column2 {
  display: flex;
  flex: 1;  /* 子行 每行满格设置 */
  align-items: center;
  border-bottom: 1rpx solid #DCDFE6;
}
.column2:last-child{
  border-bottom: none;
}
.column3-wrapper {
  display: flex;
  flex-direction: column;
  flex: 2;  /* 子行根据后续列设置数据 (这个数据需要再计算结构的时候一同计算出来,就可以愉快使用无限套娃了) */
  justify-content: center;
  border-left: 1rpx solid #DCDFE6;
}
.column3 {
  display: flex;
  flex: 1;
  align-items: center;
  border-bottom: 1rpx solid #DCDFE6;
}
.column3:last-child{
  border-bottom: none;
}
.column-value{
  display: flex;
  align-self: flex-end;
  margin-right: 10rpx;
  padding: 10rpx;
  line-height: 60rpx;
  height: 60rpx;
}
.column4-wrapper{
  display: flex;
  flex-direction: column;
  flex: 1;
  justify-content: center;
  border-left: 1rpx solid #DCDFE6;
}
.picker-content{
  display: flex;
  width: 100%;
  justify-content: center;
  align-items: center;
  border-bottom: 1rpx solid rgba(7, 17, 27, 0.1);
}
.date-icon{
  width: 80rpx;
  height: 80rpx;
}
.nodata{
  width: 100%;
  text-align: center;
  font-size: 32rpx;
  color: #666;
  padding: 20rpx;
}

总结:通过构建数据格式来实现需要的效果一直以来都是数据驱动模板的核心思想。 此组件嵌套的不管是 节点元素 还是数据格式层级,都是较为复杂的。

webfansplz commented 3 years ago

Repository (仓库地址):https://github.com/webpack-contrib/file-loader Gain (收获) :

源码解析:


/**
 * file-loader 并不会对文件内容进行任何转换,只是复制一份文件内容,并根据配置生成唯一的文件名。
 * 工作流程:
 * 1. 通过 loaderUtils.interpolateName 方法根据 options.name 以及文件内容生成唯一的文件名 url
 * 2. 通过 this.emitFile(url, content) 告诉 webpack 我需要创建一个文件,webpack会根据参数创建对应的文件
 * 3. 将原先文件路径替换为编译后文件路径
 */

import path from "path";

/**
 * getOptions 获取 loader 的配置项。
 * interpolateName 处理生成文件的名字。
 */
import { getOptions, interpolateName } from "loader-utils";

// 验证 loader option 配置的合法性
import { validate } from "schema-utils";

import schema from "./options.json";
import { normalizePath } from "./utils";

export default function loader(content) {
  const options = getOptions(this);
  // 验证配置是否符合要求
  validate(schema, options, {
    name: "File Loader",
    baseDataPath: "options",
  });

  const context = options.context || this.rootContext;
  const name = options.name || "[contenthash].[ext]";
  // 根据 options.name 以及文件内容生成唯一的文件名 url
  const url = interpolateName(this, name, {
    context,
    content,
    regExp: options.regExp,
  });

  let outputPath = url;
  // 指定放置目标文件的文件路径
  if (options.outputPath) {
    if (typeof options.outputPath === "function") {
      outputPath = options.outputPath(url, this.resourcePath, context);
    } else {
      outputPath = path.posix.join(options.outputPath, url);
    }
  }
  // 把原来的文件路径替换为编译后的路径
  // __webpack_public_path__ ,这是一个由webpack提供的全局变量,是public的根路径
  let publicPath = `__webpack_public_path__ + ${JSON.stringify(outputPath)}`;
  // // 指定放置目标文件的公共路径
  if (options.publicPath) {
    if (typeof options.publicPath === "function") {
      publicPath = options.publicPath(url, this.resourcePath, context);
    } else {
      publicPath = `${
        options.publicPath.endsWith("/")
          ? options.publicPath
          : `${options.publicPath}/`
      }${url}`;
    }

    publicPath = JSON.stringify(publicPath);
  }
  // 指定一个自定义函数来对生成的公共路径进行后处理
  if (options.postTransformPublicPath) {
    publicPath = options.postTransformPublicPath(publicPath);
  }
  // 是否输出文件,默认为是
  if (typeof options.emitFile === "undefined" || options.emitFile) {
    const assetInfo = {};

    if (typeof name === "string") {
      let normalizedName = name;

      const idx = normalizedName.indexOf("?");

      if (idx >= 0) {
        normalizedName = normalizedName.substr(0, idx);
      }

      const isImmutable = /\[([^:\]]+:)?(hash|contenthash)(:[^\]]+)?]/gi.test(
        normalizedName
      );

      if (isImmutable === true) {
        assetInfo.immutable = true;
      }
    }

    assetInfo.sourceFilename = normalizePath(
      path.relative(this.rootContext, this.resourcePath)
    );
    // 告诉 webpack 我需要创建一个文件,webpack会根据参数创建对应的文件,放在 public path 目录下。
    this.emitFile(outputPath, content, null, assetInfo);
  }

  const esModule =
    typeof options.esModule !== "undefined" ? options.esModule : true;
  // esm or cmjs export, default esm
  // 在一些情况下,使用esm模块化是有益的,比如Tree Shaking 和 Scope Hoisting。

  return `${esModule ? "export default" : "module.exports ="} ${publicPath};`;
}

// 默认情况下 webpack 会把文件内容当做UTF8字符串处理,而我们的文件是二进制的,当做UTF8会导致图片格式错误。
// 因此我们需要指定webpack用 raw-loader 来加载文件的内容,而不是当做 UTF8 字符串传给我们
export const raw = true;