Banana-FE / github-weekly

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

2020-Nov-Week3 #6

Open webfansplz opened 4 years ago

webfansplz commented 4 years ago

Share your knowledge and repository sources from Github . ♥️

2020/11/16 - 2020/11/22 ~

AzTea commented 4 years ago

Repository (仓库地址):https://github.com/zenorocha/clipboard.js Gain (收获) : iOS复制兼容问题

1.为什么引入该库

原复制代码(在iOS12.4无法复制)

let url = data;
let oInput = document.createElement("input");
oInput.value = url;
document.body.appendChild(oInput);
oInput.select(); // 选择对象;
console.log(oInput.value);
document.execCommand("Copy"); // 执行浏览器复制命令
oInput.remove();

2.使用

官网demo(input)

<!-- Target -->
<input id="foo" type="text" value="hello">

<!-- Trigger -->
<button class="btn" data-clipboard-action="copy" data-clipboard-target="#foo">Copy</button>
var clipboard = new ClipboardJS('.btn');

    clipboard.on('success', function(e) {
        console.log(e);
    });

    clipboard.on('error', function(e) {
        console.log(e);
    });

从btn点击到复制,走了三步:trigger > target > copy

3.源码分析

clipboardjs源码包含两个核心文件clipboard.js、clipboard-action.js以及tiny-emitter.js

3.1 tiny-emitter.js

tiny-emitter.js是一个事件发射器,从demo中可以看到定义了事件以及在触发事件之后success和error的标识,clipboard用tiny-emitter.js的on和emit方法来处理复制的回调

// clipboard-action.js(emit方法)
handleResult(succeeded) {
    this.emitter.emit(succeeded ? 'success' : 'error', {
        action: this.action,
        text: this.selectedText,
        trigger: this.trigger,
        clearSelection: this.clearSelection.bind(this)
    });
}

3.2 clipboard.js

clipboard.js负责接收参数并封装

import ClipboardAction from './clipboard-action';
import Emitter from 'tiny-emitter';
import listen from 'good-listener';

/**
 * Base class which takes one or more elements, adds event listeners to them,
 * and instantiates a new `ClipboardAction` on each click.
 */
class Clipboard extends Emitter {
    /**
     * @param {String|HTMLElement|HTMLCollection|NodeList} trigger
     * @param {Object} options
     */
    constructor(trigger, options) {
        super();

        //定义属性
        this.resolveOptions(options);
        //定义事件
        this.listenClick(trigger);
    }

    /**
     * Defines if attributes would be resolved using internal setter functions
     * or custom functions that were passed in the constructor.
     * @param {Object} options
     */
    resolveOptions(options = {}) {
        //事件行为
        this.action    = (typeof options.action    === 'function') ? options.action    : this.defaultAction;
        //复制目标
        this.target    = (typeof options.target    === 'function') ? options.target    : this.defaultTarget;
        //复制内容
        this.text      = (typeof options.text      === 'function') ? options.text      : this.defaultText;
        //包含元素
        this.container = (typeof options.container === 'object')   ? options.container : document.body;
    }

    /**
     * Adds a click event listener to the passed trigger.
     * @param {String|HTMLElement|HTMLCollection|NodeList} trigger
     */
    listenClick(trigger) {
        this.listener = listen(trigger, 'click', (e) => this.onClick(e));
    }

    /**
     * Defines a new `ClipboardAction` on each click event.
     * @param {Event} e
     */
    onClick(e) {
        const trigger = e.delegateTarget || e.currentTarget;

        if (this.clipboardAction) {
            this.clipboardAction = null;
        }

        this.clipboardAction = new ClipboardAction({
            action    : this.action(trigger),
            target    : this.target(trigger),
            text      : this.text(trigger),
            container : this.container,
            trigger   : trigger,
            emitter   : this
        });
    }

    /**
     * Default `action` lookup function.
     * @param {Element} trigger
     */
    defaultAction(trigger) {
        return getAttributeValue('action', trigger);
    }

    /**
     * Default `target` lookup function.
     * @param {Element} trigger
     */
    defaultTarget(trigger) {
        const selector = getAttributeValue('target', trigger);

        if (selector) {
            return document.querySelector(selector);
        }
    }

    /**
     * Returns the support of the given action, or all actions if no action is
     * given.
     * @param {String} [action]
     */
    static isSupported(action = ['copy', 'cut']) {
        const actions = (typeof action === 'string') ? [action] : action;
        let support = !!document.queryCommandSupported;

        actions.forEach((action) => {
            support = support && !!document.queryCommandSupported(action);
        });

        return support;
    }

    /**
     * Default `text` lookup function.
     * @param {Element} trigger
     */
    defaultText(trigger) {
        return getAttributeValue('text', trigger);
    }

    /**
     * Destroy lifecycle.
     */
    destroy() {
        this.listener.destroy();

        if (this.clipboardAction) {
            this.clipboardAction.destroy();
            this.clipboardAction = null;
        }
    }
}

/**
 * Helper function to retrieve attribute value.
 * @param {String} suffix
 * @param {Element} element
 */
function getAttributeValue(suffix, element) {
    const attribute = `data-clipboard-${suffix}`;

    if (!element.hasAttribute(attribute)) {
        return;
    }

    return element.getAttribute(attribute);
}

export default Clipboard;

从clipboard.js中的resolveOptions可以极为清晰的看到格式化了4个参数:action、target、text、container。格式化时判断是否传递了相应的参数,传递了就使用,没有的话就从trigger元素中通过属性获取(data-clipboard-xxx)

3.3 clipboard-action.js

核心代码initSelection:初始化选择(使用哪一种策觉取决于提供的text和target)

initSelection() {
    if (this.text) {
        this.selectFake();
    }
    else if (this.target) {
        this.selectTarget();
    }
}

selectTarget(实现了浏览器复制的步骤:点击 - 选中 - 复制)。selectTarget中调用的select方法,选中之后,进行复制操作(copyText方法)


function select(element) {
    var selectedText;
    // target为select时
    if (element.nodeName === 'SELECT') {
        // 选中
        element.focus();
        // 记录值
        selectedText = element.value;
    }
    // target为input或者textarea时
    else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
        var isReadOnly = element.hasAttribute('readonly');
        // 如果属性为只读,不能选中
        if (!isReadOnly) {
            element.setAttribute('readonly', '');
        }
        // 选中target
        element.select();
        // 设置选中target的范围
        element.setSelectionRange(0, element.value.length);

        if (!isReadOnly) {
            element.removeAttribute('readonly');
        }
        // 记录值
        selectedText = element.value;
    }
    else {
        if (element.hasAttribute('contenteditable')) {
            element.focus();
        }
        // 创建getSelection,用来选中除input、testarea、select元素
        var selection = window.getSelection();
        // 创建createRange,用来设置getSelection的选中范围
        var range = document.createRange();

        // 选中范围设置为target元素
        range.selectNodeContents(element);

        // 清空getSelection已选中的范围
        selection.removeAllRanges();

        // 把target元素设置为getSelection的选中范围
        selection.addRange(range);

        // 记录值
        selectedText = selection.toString();
    }

    return selectedText;
}
/**
* 对目标执行复制操作
*/
copyText() {
    let succeeded;

    try {
        succeeded = document.execCommand(this.action);
    }
    catch (err) {
        succeeded = false;
    }

    this.handleResult(succeeded);
}
scorpioLh commented 4 years ago

Repository (仓库地址):https://github.com/sindresorhus/screenfull.js Gain (收获) : 网页全屏解决方法。作者将兼容、组件的函数、属性分别存放,让源码更简洁易读懂,一目了然。

(function () {
    'use strict';

    // 获取document节点,没获取到的话就创建一个document对象
    var document = typeof window !== 'undefined' && typeof window.document !== 'undefined' ? window.document : {};
    var isCommonjs = typeof module !== 'undefined' && module.exports;

    var fn = (function () {
        var val;

        // 为了兼容不同浏览器内核
        var fnMap = [
            [
                'requestFullscreen',
                'exitFullscreen',
                'fullscreenElement',
                'fullscreenEnabled',
                'fullscreenchange',
                'fullscreenerror'
            ],
            // New WebKit
            [
                'webkitRequestFullscreen',
                'webkitExitFullscreen',
                'webkitFullscreenElement',
                'webkitFullscreenEnabled',
                'webkitfullscreenchange',
                'webkitfullscreenerror'

            ],
            // Old WebKit
            [
                'webkitRequestFullScreen',
                'webkitCancelFullScreen',
                'webkitCurrentFullScreenElement',
                'webkitCancelFullScreen',
                'webkitfullscreenchange',
                'webkitfullscreenerror'

            ],
            [
                'mozRequestFullScreen',
                'mozCancelFullScreen',
                'mozFullScreenElement',
                'mozFullScreenEnabled',
                'mozfullscreenchange',
                'mozfullscreenerror'
            ],
            [
                'msRequestFullscreen',
                'msExitFullscreen',
                'msFullscreenElement',
                'msFullscreenEnabled',
                'MSFullscreenChange',
                'MSFullscreenError'
            ]
        ];

        var i = 0;
        var l = fnMap.length;
        var ret = {};

        for (; i < l; i++) {
            val = fnMap[i];
            if (val && val[1] in document) {
                for (i = 0; i < val.length; i++) {
                    ret[fnMap[0][i]] = val[i];
                }
                return ret;
            }
        }

        return false;
    })();

    var eventNameMap = {
        change: fn.fullscreenchange,
        error: fn.fullscreenerror
    };

    var screenfull = {
        /**
         * 接受一个DOM作为参数,默认是<html>,
         * 用户进入全屏后解绑change事件并返回resolve
         */
        request: function (element) {
            return new Promise(function (resolve, reject) {
                var onFullScreenEntered = function () {
                    this.off('change', onFullScreenEntered);
                    resolve();
                }.bind(this);

                this.on('change', onFullScreenEntered);

                element = element || document.documentElement;

                var returnPromise = element[fn.requestFullscreen]();

                if (returnPromise instanceof Promise) {
                    returnPromise.then(onFullScreenEntered).catch(reject);
                }
            }.bind(this));
        },

        /**
         * 退出全屏,并返回resolve
         */
        exit: function () {
            return new Promise(function (resolve, reject) {
                if (!this.isFullscreen) {
                    resolve();
                    return;
                }

                var onFullScreenExit = function () {
                    this.off('change', onFullScreenExit);
                    resolve();
                }.bind(this);

                this.on('change', onFullScreenExit);

                var returnPromise = document[fn.exitFullscreen]();

                if (returnPromise instanceof Promise) {
                    returnPromise.then(onFullScreenExit).catch(reject);
                }
            }.bind(this));
        },

        /**
         * 如果是全屏则退出,否则发起全屏请求
         * @param {*} element 
         */
        toggle: function (element) {
            return this.isFullscreen ? this.exit() : this.request(element);
        },

        /**
         * 相当于.on('change', function) 函数
         * @param {*} callback 
         */
        onchange: function (callback) {
            this.on('change', callback);
        },

        /**
         * 相当于.on('error', function) 函数
         * @param {*} callback 
         */
        onerror: function (callback) {
            this.on('error', callback);
        },

        /**
         * 添加监听,当用户进入全屏或报错时执行
         * @param {*} event 
         * @param {*} callback 
         */
        on: function (event, callback) {
            var eventName = eventNameMap[event];
            if (eventName) {
                document.addEventListener(eventName, callback, false);
            }
        },

        /**
         * 移除之前注册的监听
         * @param {*} event 
         * @param {*} callback 
         */
        off: function (event, callback) {
            var eventName = eventNameMap[event];
            if (eventName) {
                document.removeEventListener(eventName, callback, false);
            }
        },

        /**
         * 展示需要兼容处理的属性(例如需要加前缀)
         */
        raw: fn
    };

    if (!fn) {
        if (isCommonjs) {
            module.exports = {isEnabled: false};
        } else {
            window.screenfull = {isEnabled: false};
        }

        return;
    }

    Object.defineProperties(screenfull, {
        /**
         * 返回一个boolean类型告知当前是否为全屏
         */
        isFullscreen: {
            get: function () {
                return Boolean(document[fn.fullscreenElement]);
            }
        },

        /**
         * 返回当前全屏元素,或者null
         */
        element: {
            enumerable: true,
            get: function () {
                return document[fn.fullscreenElement];
            }
        },

        /**
         * 返回一个布尔值,是否允许进入全屏。
         */
        isEnabled: {
            enumerable: true,
            get: function () {
                // Coerce to boolean in case of old WebKit
                return Boolean(document[fn.fullscreenEnabled]);
            }
        }
    });

    if (isCommonjs) {
        module.exports = screenfull;
    } else {
        window.screenfull = screenfull;
    }
})();

注意:出于安全原因,浏览器不支持自动全屏。只有在用户事件(如单击、触摸、按键)启动时,浏览器才会进入全屏。 但是,有一个肮脏的变通办法。创建一个无缝iframe,填充屏幕并在其中导航到页面。

$('#new-page-btn').click(() => {
    const iframe = document.createElement('iframe')

    iframe.setAttribute('id', 'external-iframe');
    iframe.setAttribute('src', 'https://new-page-website.com');
    iframe.setAttribute('frameborder', 'no');
    iframe.style.position = 'absolute';
    iframe.style.top = '0';
    iframe.style.right = '0';
    iframe.style.bottom = '0';
    iframe.style.left = '0';
    iframe.style.width = '100%';
    iframe.style.height = '100%';

    $(document.body).prepend(iframe);
    document.body.style.overflow = 'hidden';
})
webfansplz commented 4 years ago

Repository (仓库地址): https://github.com/vuejs/vue-next/blob/master/packages/compiler-sfc/src/compileStyle.ts

Gain (收获) :

源码分析:

import postcss, {
  ProcessOptions,
  LazyResult,
  Result,
  ResultMap,
  ResultMessage,
} from "postcss";
import trimPlugin from "./stylePluginTrim";
import scopedPlugin from "./stylePluginScoped";
import {
  processors,
  StylePreprocessor,
  StylePreprocessorResults,
  PreprocessLang,
} from "./stylePreprocessors";
import { RawSourceMap } from "source-map";
import { cssVarsPlugin } from "./cssVars";

export interface SFCStyleCompileOptions {
  source: string;
  filename: string;
  id: string;
  scoped?: boolean;
  trim?: boolean;
  isProd?: boolean;
  inMap?: RawSourceMap;
  preprocessLang?: PreprocessLang;
  preprocessOptions?: any;
  preprocessCustomRequire?: (id: string) => any;
  postcssOptions?: any;
  postcssPlugins?: any[];
  /**
   * @deprecated
   */
  map?: RawSourceMap;
}

export interface SFCAsyncStyleCompileOptions extends SFCStyleCompileOptions {
  isAsync?: boolean;
  // css modules support, note this requires async so that we can get the
  // resulting json
  modules?: boolean;
  // maps to postcss-modules options
  // https://github.com/css-modules/postcss-modules
  modulesOptions?: {
    scopeBehaviour?: "global" | "local";
    globalModulePaths?: string[];
    generateScopedName?:
      | string
      | ((name: string, filename: string, css: string) => string);
    hashPrefix?: string;
    localsConvention?: "camelCase" | "camelCaseOnly" | "dashes" | "dashesOnly";
  };
}

export interface SFCStyleCompileResults {
  code: string;
  map: RawSourceMap | undefined;
  rawResult: LazyResult | Result | undefined;
  errors: Error[];
  modules?: Record<string, string>;
  dependencies: Set<string>;
}

export function compileStyle(
  options: SFCStyleCompileOptions
): SFCStyleCompileResults {
  return doCompileStyle({
    ...options,
    isAsync: false,
  }) as SFCStyleCompileResults;
}

export function compileStyleAsync(
  options: SFCAsyncStyleCompileOptions
): Promise<SFCStyleCompileResults> {
  return doCompileStyle({
    ...options,
    isAsync: true,
  }) as Promise<SFCStyleCompileResults>;
}
// 编译style
export function doCompileStyle(
  options: SFCAsyncStyleCompileOptions
): SFCStyleCompileResults | Promise<SFCStyleCompileResults> {
  const {
    filename,
    id,
    scoped = false,
    trim = true,
    isProd = false,
    modules = false,
    modulesOptions = {},
    preprocessLang,
    postcssOptions,
    postcssPlugins,
  } = options;
  // 核心 1.
  // 找到对应的css预编译器('less' | 'sass' | 'scss' | 'styl' | 'stylus')
  const preprocessor = preprocessLang && processors[preprocessLang];
  // 通过对应的预编译器render方法,拿到转换后的css代码资源
  const preProcessedSource = preprocessor && preprocess(options, preprocessor);
  // sourcemap
  const map = preProcessedSource
    ? preProcessedSource.map
    : options.inMap || options.map;
  const source = preProcessedSource ? preProcessedSource.code : options.source;

  const shortId = id.replace(/^data-v-/, "");
  const longId = `data-v-${shortId}`;
  // plugin list init,default postcssPlugins
  const plugins = (postcssPlugins || []).slice();
  // add cssVarsPlugin
  plugins.unshift(cssVarsPlugin({ id: shortId, isProd }));
  // add trimPlugin
  if (trim) {
    plugins.push(trimPlugin());
  }
  // add scopedPlugin
  if (scoped) {
    plugins.push(scopedPlugin(longId));
  }
  let cssModules: Record<string, string> | undefined;
  // css modules plugin
  if (modules) {
    if (__GLOBAL__ || __ESM_BROWSER__) {
      throw new Error(
        "[@vue/compiler-sfc] `modules` option is not supported in the browser build."
      );
    }
    if (!options.isAsync) {
      throw new Error(
        "[@vue/compiler-sfc] `modules` option can only be used with compileStyleAsync()."
      );
    }
    plugins.push(
      require("postcss-modules")({
        ...modulesOptions,
        getJSON: (_cssFileName: string, json: Record<string, string>) => {
          cssModules = json;
        },
      })
    );
  }

  const postCSSOptions: ProcessOptions = {
    ...postcssOptions,
    to: filename,
    from: filename,
  };
  if (map) {
    postCSSOptions.map = {
      inline: false,
      annotation: false,
      prev: map,
    };
  }

  let result: LazyResult | undefined;
  let code: string | undefined;
  let outMap: ResultMap | undefined;
  // stylus output include plain css. so need remove the repeat item
  const dependencies = new Set(
    preProcessedSource ? preProcessedSource.dependencies : []
  );
  // sass has filename self when provided filename option
  dependencies.delete(filename);

  const errors: Error[] = [];
  if (preProcessedSource && preProcessedSource.errors.length) {
    errors.push(...preProcessedSource.errors);
  }

  const recordPlainCssDependencies = (messages: ResultMessage[]) => {
    messages.forEach((msg) => {
      if (msg.type === "dependency") {
        // postcss output path is absolute position path
        dependencies.add(msg.file);
      }
    });
    return dependencies;
  };

  try {
    // 核心 2.
    // postcss css后处理器
    // css-> css parser -> plugin system -> stringifier-> result css
    // 把预编译器生成的css传给css parser生成ast,再用对应插件选项进行解析转换,最后转成string结果。
    result = postcss(plugins).process(source, postCSSOptions);

    // In async mode, return a promise.
    if (options.isAsync) {
      return result
        .then((result) => ({
          code: result.css || "",
          map: result.map && (result.map.toJSON() as any),
          errors,
          modules: cssModules,
          rawResult: result,
          dependencies: recordPlainCssDependencies(result.messages),
        }))
        .catch((error) => ({
          code: "",
          map: undefined,
          errors: [...errors, error],
          rawResult: undefined,
          dependencies,
        }));
    }

    recordPlainCssDependencies(result.messages);
    // force synchronous transform (we know we only have sync plugins)
    code = result.css;
    outMap = result.map;
  } catch (e) {
    errors.push(e);
  }

  return {
    code: code || ``,
    map: outMap && (outMap.toJSON() as any),
    errors,
    rawResult: result,
    dependencies,
  };
}

function preprocess(
  options: SFCStyleCompileOptions,
  preprocessor: StylePreprocessor
): StylePreprocessorResults {
  if ((__ESM_BROWSER__ || __GLOBAL__) && !options.preprocessCustomRequire) {
    throw new Error(
      `[@vue/compiler-sfc] Style preprocessing in the browser build must ` +
        `provide the \`preprocessCustomRequire\` option to return the in-browser ` +
        `version of the preprocessor.`
    );
  }

  return preprocessor(
    options.source,
    options.map,
    {
      filename: options.filename,
      ...options.preprocessOptions,
    },
    options.preprocessCustomRequire
  );
}
wenfeihuazha commented 4 years ago

Repository (仓库地址):https://github.com/vuejs/vue Gain (收获) : vue库太大了,所以这次先分享一个mixin的核心代码~ 总结

diandiantong commented 4 years ago

分享到各个平台的插件:https://github.com/zhansingsong/iShare.js 收获:除了微信分享需要 SDK 之外,其他的各个平台都可以通过 url 实现。从这里可以思考到,没有后端做配合的验证配置都是不安全的 使用直接访问插件 git

核心各个平台配置 URL :

var _templates = {
    iShare_qq          : 'http://connect.qq.com/widget/shareqq/index.html?url={{URL}}&title={{TITLE}}&desc={{DESCRIPTION}}&summary=&pics={{IMAGE}}',
    iShare_qzone       : 'http://sns.qzone.qq.com/cgi-bin/qzshare/cgi_qzshare_onekey?url={{URL}}&title={{TITLE}}&summary={{DESCRIPTION}}&pics={{IMAGE}}&desc=&site=',
    iShare_tencent     : 'http://share.v.t.qq.com/index.php?c=share&a=index&title={{TITLE}}&url={{URL}}&pic={{IMAGE}}',
    iShare_weibo       : 'http://service.weibo.com/share/share.php?url={{URL}}&title={{TITLE}}&pic={{IMAGE}}',
    iShare_wechat      : 'http://s.jiathis.com/qrcode.php?url={{URL}}',
    iShare_douban      : 'http://shuo.douban.com/!service/share?href={{URL}}&name={{TITLE}}&text={{DESCRIPTION}}&image={{IMAGE}}',
    iShare_renren            : 'http://widget.renren.com/dialog/share?resourceUrl={{URL}}&title={{TITLE}}&pic={{IMAGE}}&description={{DESCRIPTION}}',
    iShare_youdaonote  : 'http://note.youdao.com/memory/?title={{TITLE}}&pic={{IMAGE}}&summary={{DESCRIPTION}}&url={{URL}}',
    iShare_linkedin    : 'http://www.linkedin.com/shareArticle?mini=true&ro=true&title={{TITLE}}&url={{URL}}&summary={{DESCRIPTION}}&armin=armin',
    iShare_facebook    : 'https://www.facebook.com/sharer/sharer.php?s=100&p[title]={{TITLE}}p[summary]={{DESCRIPTION}}&p[url]={{URL}}&p[images]={{IMAGE}}',
    iShare_twitter     : 'https://twitter.com/intent/tweet?text={{TITLE}}&url={{URL}}',
    iShare_googleplus  : 'https://plus.google.com/share?url={{URL}}&t={{TITLE}}',
    iShare_pinterest     : 'https://www.pinterest.com/pin/create/button/?url={{URL}}&description={{DESCRIPTION}}&media={{IMAGE}}',
    iShare_tumblr            : 'https://www.tumblr.com/widgets/share/tool?shareSource=legacy&canonicalUrl=&url={{URL}}&title={{TITLE}}'
},

初始化值(iShare):

var defaults = {
    title       : document.title,
    url         : location.href,
    host        : location.origin || '',
    description : Util.getmeta('description'),
    image       : Util.getimg(),
    sites       : ['iShare_weibo','iShare_qq','iShare_wechat','iShare_tencent','iShare_douban','iShare_qzone','iShare_renren','iShare_youdaonote','iShare_facebook','iShare_linkedin','iShare_twitter','iShare_googleplus','iShare_tumblr','iShare_pinterest'],
    initialized : true,
    isTitle     : true,
    isAbroad    : false,
    WXoptions   : {}
};

这里微信需要另外处理,所以需要判定是否微信内 new WX(_e, url, this.settings.WXoptions); 微信判定分享类:

/**
 * WX 微信类
 * @param {DOMObject} element 微信按钮节点
 * @param {object}      options 配置项
 * 
 */
 function WX(element, URL, options) {
    this.element = element;
    this.wxbox = document.createElement('div');
    // 配置项
    this.URL = URL;
    this.style = options.style;
    this.bgcolor = options.bgcolor;
    this.evenType = options.evenType || 'mouseover'; // 默认触发方式
    this.isTitleVisibility = (options.isTitleVisibility === void(0)) ? true : options.isTitleVisibility; // 是否有标题
    this.title = options.title || '分享到微信';
    this.isTipVisibility = (options.isTipVisibility === void(0)) ? true : options.isTipVisibility; // 是否有提示
    this.tip = options.tip || '打开微信,使用 “扫一扫” 即可将网页分享到朋友圈。';
    this.upDownFlag = '';// 保存up|down
    this.status = false; // 保存状态
    this.visibility = false;// 保存可见性
 }
 WX.prototype = function() {
    return{
        constructor: WX,
        init: function() {
            this.render();
            this.init = this.show;
            this.bindEvent();
        },
        render: function(){
            var _upFlag = '',
                    _downFlag = '',
                    // _widthStyle = (!this.isTitleVisibility || !this.isTipVisibility) ? 'width: 110px;' : 'width : 150px;',
                    _imgStyle = '',//待定
                    _titleStyle = '',//待定
                    _tipStyle = '', //待定
                    _bgcolor = this.bgcolor ? this.bgcolor : '#ddd',
                    _radius = '';
            // 判断上下
            if (Util.getWinDimension().pageHeight / 2 < Util.getElementTop(this.element)) {
                _downFlag = '';
                _upFlag = 'display:none;';
                this.upDownFlag = 'down';
                _radius = 'border-bottom-left-radius: 0;';
            } else {
                _downFlag = 'display:none;';
                _upFlag = '';
                this.upDownFlag = 'up';
                _radius = 'border-top-left-radius: 0;';
            }
            
            var _containerHTML = '<div style="text-align: center;background-color: ' + _bgcolor + ';box-shadow: 1px 1px 4px #888888;padding: 8px 8px 4px;border-radius: 4px;' + _radius + '">',
                    _titleHTML = this.isTitleVisibility ?  '<p class="tt" style="line-height: 30px;margin:0; text-shadow: 1px 1px rgba(0,0,0,0.1);font-weight: 700;margin-bottom: 4px;' + _titleStyle + '">' + this.title + '</p>' : '',
                    _imgHTML = '<img  style="font-size: 12px;line-height: 20px; -webkit-user-select: none;box-shadow: 1px 1px 2px rgba(0,0,0,0.4); ' + _imgStyle + '" src="' + this.URL + '">',
                    _tipHTML = this.isTipVisibility ? '<p style="font-size: 12px;line-height: 20px; margin: 4px auto;width: 120px;' + _tipStyle + '">' + this.tip + '</p>' : '',
                    _upArrowHTML = '<div style="' + _upFlag + 'position: relative;height: 0;width: 0;border-style: solid;border-width: 12px;border-color: transparent;border-bottom-color: ' + _bgcolor + ';border-top: none;"></div>',
                    _downArrowHTML = '</div><div style="' + _downFlag + 'position: relative;height: 0;width: 0;border-style: solid;border-width: 12px;border-color: transparent;border-top-color: ' + _bgcolor + ';border-bottom: none;"></div>';
      // 拼接WXHTML
      var WXSTR = _upArrowHTML + _containerHTML + _titleHTML + _imgHTML + _tipHTML + _downArrowHTML;
      this.wxbox.innerHTML = WXSTR;
      this.wxbox.style.cssText = 'position:absolute; left: -99999px;';
      document.body.appendChild(this.wxbox);
        },
        setLocation: function(flag){
            // 渲染后再调整位置
            var _boxW = this.wxbox.offsetWidth,
                    _boxH = this.wxbox.offsetHeight,
                    _eW = this.element.offsetWidth,
                    _eH = this.element.offsetHeight,
                    _eTop = Util.getElementTop(this.element),
                    _eLeft = Util.getElementLeft(this.element),
                    _boxStyle = 'position:absolute; color: #000;z-index: 99999;';
            
            _boxStyle = _boxStyle + 'left: ' + ( _eW / 2 - 12 + _eLeft) + 'px;';
            if(this.upDownFlag === 'down'){
                _boxStyle = _boxStyle + 'top: ' + (_eTop - _boxH) + 'px;';
            } else {
                _boxStyle = _boxStyle + 'top: ' + (_eTop + _eH) + 'px;';
            }
            this.wxbox.style.cssText = _boxStyle + this.style;
            flag && (this.hide());
        },
        bindEvent: function() {
            var _me = this;
            if(this.evenType === 'click'){
                Util.event.addEvent(this.element, 'click', function(e){
                    var event = e || window.event;
                    Util.event.stopPropagation(event);
                    Util.event.preventDefault(event);
                    if(!_me.visibility){
                        _me.show();
                    } else {
                        _me.hide();
                    }
                });
            } else {
                Util.event.addEvent(this.element, 'mouseover', function(e){
                    var event = e || window.event;
                    Util.event.stopPropagation(event);
                    _me.show();
                });
                Util.event.addEvent(this.element, 'mouseout', function(e){
                    var event = e || window.event;
                    Util.event.stopPropagation(event);
                    _me.hide();
                });
            }
            Util.event.addEvent(window, 'resize', Util.throttle(function(){
                (_me.status) && (_me.visibility) && (_me.setLocation());
            }, 200));
        },
        show: function(){
            this.status = true;
            this.wxbox.style.display = 'block';
            this.visibility = true;
            this.show = function(){
                this.wxbox.style.display = 'block';
                this.visibility = true;
            }
        },
        hide: function() {
          this.wxbox.style.display = 'none';
          this.visibility = false;
        }
    };  
 }();

监听初始化:

event: {
    addEvent: function(element, type, handler){
        if(element.addEventListener){
            element.addEventListener(type, handler, false);
        } else if(element.attachEvent){
            element.attachEvent('on' + type, handler);
        } else {
            element['on' + type] = handler;
        }
    },
    removeEvent: function(element, type, handler){
        if(element.removeEventListener){
            element.removeEventListener(type, handler, false);
        } else if(element.detachEvent){
            element.detachEvent('on' + type, handler);
        } else {
            element['on' + type] = null;
        }
    },
    stopPropagation: function(event){
        if(event.stopPropagation) {
          event.stopPropagation();
          }else {
          event.cancelBubble = true;
          }
    },
    preventDefault: function(event){
        if(event.preventDefault){
            event.preventDefault();
        } else {
            event.returnValue = false;
        }
    }
},
rxyshww commented 4 years ago

Repository (仓库地址): vue-next/renderer Gain (收获) : 使用最长递增子序列算法来减少diff时对dom的操作

源码分析:

我们先看下面的的代码,代表dom发生变化的前后:

    h('ul', { style: {color: 'red'}}, [
        h('li', {key: 'A', style: {background: 'red'}}, 'A'),
        h('li', {key: 'B', style: {background: 'red'}}, 'B'),

        h('li', {key: 'C', style: {background: 'red'}}, 'C'),
        h('li', {key: 'D', style: {background: 'red'}}, 'D'),
        h('li', {key: 'E', style: {background: 'red'}}, 'E'),

        h('li', {key: 'F', style: {background: 'red'}}, 'F'),
        h('li', {key: 'G', style: {background: 'red'}}, 'G')
    ])

    h('ul', { style: {color: 'red'}}, [
        h('li', {key: 'A', style: {background: 'red'}}, 'A'),
        h('li', {key: 'B', style: {background: 'red'}}, 'B'),

        h('li', {key: 'D', style: {background: 'red'}}, 'D'),
        h('li', {key: 'E', style: {background: 'red'}}, 'E'),
        h('li', {key: 'C', style: {background: 'red'}}, 'C'),
        h('li', {key: 'H', style: {background: 'red'}}, 'H'),

        h('li', {key: 'F', style: {background: 'red'}}, 'F'),
        h('li', {key: 'G', style: {background: 'red'}}, 'G')
    ])

在上次分析中我们得知,先做一次正序循环,得知AB只需要更新属性,倒序循环,同理得知FG也只需要更新属性。 而中间的CDE则要变化为DECH

顺序倒序循环后我们得到以下数据:(不知道啥意思可以看上一遍文章,当然看下面的代码也能大概猜出啥意思)

// ab [cde] fg     // s1 = 2    e1 = 4
// ab [dech] fg    // s2 = 2    e2 = 5

// 我们需要先把 [dech]的每一项和他在列表中的索引做一个映射关系
const keyToNewIndexMap = new Map();
for (let i = s2; i <= e2; i++) {
    const nextChild = c2[i];
    keyToNewIndexMap.set(nextChild.key, i);
}

// Map => {D => 2, E => 3, C => 4, H => 5}

// 然后我们将新的要生成的元素生成一个数组,来确定,这些元素在不在旧的列表中
// 如果不在,则说明是新元素
const toBePatched = e2 - s2 + 1;     // 新生成数组元素,长度为[dech]的长度,e2 - s2 + 1 = 4
const newIndexToOldMapIndex = new Array(toBePatched).fill(0);     //用0填充
// 循环旧的数据列表
for (let i = s1; i <= e1; i++) {
    const prevChild = c[i];
    let newIndex = keyToNewIndexMap.get(prevChild.key);
    //这项能在新元素列表拿不到,说明不需要了,直接删除dom
    if (newIndex === undefined) {
        hostRemove(prevChild.el);
    } else {
       // 记录新的元素在旧列表中的索引(+1,+1是因为i可能为0,我们需要通过判断`newIndexToOldMapIndex`中哪些不为零得知他在旧列表中
        newIndexToOldMapIndex[newIndex - s2] = i + 1;
    }
}

// 得到newIndexToOldMapIndex的数组  [4,5,3,0]  表示新元素列表每一项在旧列表中的索引+1,0表示在旧列表中没有

// 我们现在处理新数据 ab [dech] fg中的[dech]  
// 
for (let i = toBePatched - 1; i >= 0; i--) {
    const nextIndex = s2 + i;   //[dech]  找到的索引
    const nextChild = c2[nextIndex];   //第一次循环,i为4 - 1 = 3;nextChild为h
    let anchor = nextIndex + 1 < c2.length ? c2[nextIndex + 1] : null;      // 拿到f,没拿到则为null

    if (newIndexToOldMapIndex[i] === 0) {
        patch(null, nextChild, el, anchor)    //如果为0,说明是新元素,直接挂载
    } else {
        // 新元素在旧列表中,直接拿出来并挂载到参照元素anchor之前,这个元素在上次文章中提到过,有疑问可以搜一搜
        move(nextChild.el, el, anchor)    //很暴力,所有节点都要动
    }
}

为什么很暴力,我们仔细想想[cde]转化为[dech],我们是对[dech]每一项都做了处理,但是,我们发现,只需要新创建h,然后把c拿出来,放到h前边就可以了。我们却多做了2步操作。VUE3中,用最长递增子序列的算法,来优化了这一操作。 上面代码中,我们得出newIndexToOldMapIndex为[4, 5, 3, 0]。 意为 ab [dech] fg(新)中[dech]每一项在 ab [cde] fg(旧)列表中的索引(+1),例如d,在旧列表中的索引是3,+1后值为4,0表示在旧列表中没有。 我们看到4,5在新旧列表里都是递增的:在新旧列表中,e都在d后边,这两个不用动,只需要操作其他dom

举个例子加深理解: 比如这个数组,用它表示新元素列表[2, 4, 5, 3, 8, 11, 6],它表示 第一个元素在旧列表中的索引为 2 - 1 = 1 第二个元素在旧列表中的索引为 4 - 1 = 3 第三个元素在旧列表中的索引为 5 - 1 = 4 第四个元素在旧列表中的索引为 3 - 1 = 2 第五个元素在旧列表中的索引为 8 - 1 = 7 第六个元素在旧列表中的索引为 11 - 1 = 10 第七个元素在旧列表中的索引为 6 - 1 = 5

我们怎样才能操作最少的dom,才能把把旧列表更新成新的? 显然 2, 4, 5, 8, 11不用动,我们只需要把旧元素的3,插入到8前面,就元素的6插入到最后边就行了 2, 4, 5, 8, 11对应索引[0, 1, 2, 4, 5],正好是不需要动的元素下标

getSequence这个方法最终返回最长递增子序列的索引集合,专门用来计算不用计算元素的最优解。 getSequence([2, 4, 5, 3, 8, 11, 6]) //[0, 1, 2, 4, 5]

同理,我们例子中[4, 5, 3, 0]算出来为[0, 1],表示第四个元素为新增,直接加到新列表最后,第三个元素不在最长递增子序列中,取出来插到第四个元素之前,第一个、第二个元素不需要动。

// getSequence([4,5,3,0])  // [0, 1] 这个结果告诉我们,0,1不用动
// 优化刚才插入的代码`move`

let increasingIndexSequence = getSequence(newIndexToOldMapIndex);  //[4,5,3,0] => [0, 1]
let j = increasingIndexSequence.length - 1;

for (let i = toBePatched - 1; i >= 0; i--) {
    const nextIndex = s2 + i;   //[dech]  找到的索引
    const nextChild = c2[nextIndex];   //第一次循环,i为4 - 1 = 3;nextChild为h
    let anchor = nextIndex + 1 < c2.length ? c2[nextIndex + 1] : null;      // 拿到f,没拿到则为null

    if (newIndexToOldMapIndex[i] === 0) {
        patch(null, nextChild, el, anchor)    //如果为0,说明是新元素,直接挂载
    } else {
        // j可能被减为负值,或者j不在increasingIndexSequence中,则需要移动
        if (j < 0 || i !== increasingIndexSequence[j]) {
            move(nextChild.el, el, anchor) 
        } else {
            j--;
        }
    }
}

getSequence源码:用了数组push+二分查找的方法时间复杂度O(nlogn)

// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
function getSequence(arr: number[]): number[] {
  const p = arr.slice()
  const result = [0]
  let i, j, u, v, c
  const len = arr.length
  for (i = 0; i < len; i++) {
    const arrI = arr[i]
    if (arrI !== 0) {
      j = result[result.length - 1]
      if (arr[j] < arrI) {
        p[i] = j
        result.push(i)
        continue
      }
      u = 0
      v = result.length - 1
      while (u < v) {
        c = ((u + v) / 2) | 0
        if (arr[result[c]] < arrI) {
          u = c + 1
        } else {
          v = c
        }
      }
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p[i] = result[u - 1]
        }
        result[u] = i
      }
    }
  }
  u = result.length
  v = result[u - 1]
  while (u-- > 0) {
    result[u] = v
    v = p[v]
  }
  return result
}

我们来分析一下这个算法逻辑

// p是一个数组,记录比当前元素小的元素的坐标(最后用到)
// result存最长递增序列的索引
// result = [0], 因为arr长度为1时,可以不做很多操作 if (arr[j] < arrI)  while (u < v) 均不成立,直接返回 [0]
// 1、循环遍历,如果当前项大于result最后一项对应的值,则把索引推进result,并在p数组将第i个元素替换成result最后一项
// j = result[result.length - 1]
// if (arr[j] < arrI) {
//   p[i] = j
//   result.push(i)
//   continue
// }
// 2、如果当前项小于result最后一项对应的值,则使用二分法获取result中第一个大于它的元素
// u = 0
// v = result.length - 1
// while (u < v) {
//   c = ((u + v) / 2) | 0
//   if (arr[result[c]] < arrI) {
//      u = c + 1
//   } else {
//      v = c
//   }
// }
// 最后 u === v 时,arr[result[u]]为数组中第一个大于arrI元素,索引为result[u]
// 3、找到以后,将p数组将第i个元素替换成result[u - 1],将result[u]替换为i
// if (u > 0) {
//   p[i] = result[u - 1]
// }
// result[u] = i

分析例子[4, 5, 3, 0]: const result = [0] p = new Array(arr.length).fill(null)
源码中为p = arr.slice(),不太好理解,要用到的值,都会被重新赋值,所以结果不受影响 开始for循环 i = 0 if (arr[j] < arrI) while (u < v) 均不成立 p = [null, null, null, null], result = [0] i = 1 (arr[j] < arrI) => (4 < 5) => true p = [null, 0, null, null], result = [0, 1] i = 2 (arr[j] < arrI) => (5 < 3) => false 二分查找后 u === v 都为0 result = [2, 1] i = 3 arrI === 0 直接跳过 result = [2, 1]中,第二个元素1,肯定是最长递增子序列中的最后一项,但是第一个元素2,明显不是 我们在i = 2 时 将[0, 1] 替换成 [2, 1]是因为2对应的值0,比0对应的值4更有潜力 如果数组长度不止为4: [4, 5, 3, 0] =》[4, 5, 3, 0, 1, 2, 6]很显然0才能和后边的1,2,6组成最长递增子序列,所以i = 2时的替换是必要的 现在我们知道result = [2, 1]中,最后一项肯定是正确的,然后我们倒序循环result,并从p中找到存的比当前值小的索引放在当前result的之前

// result = [2, 1]
// p = [null, 0, null, null];
u = result.length
v = result[u - 1]
while (u-- > 0) {
  result[u] = v
  v = p[v]
}
return result

// u = 2  v = 1 ==> u = 1; result = [2, 1]; v = p[v] => v = 0
// u = 1  v = 0 ==> u = 0; result = [0, 1];

同理分析getSequence([2, 4, 5, 3, 8, 11, 6])

getSequence([2, 4, 5, 3, 8, 11, 6])
// p = [null, null, null, null, null, null, null];  一定要理解p,它记录着arr中每个元素比他小并与他最接近的元素的下标
// 比如最后一个元素6,比他小并与他最接近的元素是5,下标为2。则对应的p最后一个值为2。
// result = [0]
// 对arr = [2, 4, 5, 3, 8, 11, 6] 正序循环
// i = 0 直接跳过 result = [0]
// i = 1 (arr[j] < arrI ==> 2 < 4 ==> true; p = [null, 0, null, null, null, null, null]; result = [0, 1]
// i = 2 (arr[j] < arrI ==> 4 < 5 ==> true; p = [null, 0, 1, null, null, null, null];result = [0, 1, 2]
// i = 3 (arr[j] < arrI ==> 5 < 3 ==> false; 二分查找 u === v 为 1; result[u - 1] = 0
//       p = [null, 0, 1, 0, null, null, null];result = [0, 1, 2]; result = [0, 3, 2]
// i = 4 (arr[j] < arrI ==> 5 < 8 ==> true; p = [null, 0, 1, 0, 2, null, null]; result = [0, 3, 2, 4]
// i = 5 (arr[j] < arrI ==> 8 < 11 ==> true; p = [null, 0, 1, 0, 2, 4, null]; result = [0, 3, 2, 4, 5]
// i = 6 (arr[j] < arrI ==> 11 < 6 ==> false ; 二分查找 u === v 为 3; result[u - 1] = 2
//       p = [null, 0, 1, 0, 2, 4, 2];result = [0, 3, 2, 6, 5]
// 循环完毕,我们知道,[0, 3, 2, 6, 5]中最后一个5,一定是最长递增子序列中最大的元素,但是前边的可能被替换调
// 所以我们需要通过回溯,将被替换的再替换回来

// result = [0, 3, 2, 6, 5]
// p = [null, 0, 1, 0, 2, 4, 2];
// u = result.length
// v = result[u - 1]
// while (u-- > 0) {
//    result[u] = v
//    v = p[v]
// }
// return result

// u = 5  v = 5 ==> u = 4; result = [0, 3, 2, 6, 5]; v = p[v] => v = 4
// u = 4  v = 4 ==> u = 3; result = [0, 3, 2, 4, 5]; v = p[v] => v = 2
// u = 3  v = 2 ==> u = 2; result = [0, 3, 2, 4, 5]; v = p[v] => v = 1
// u = 2  v = 1 ==> u = 1; result = [0, 1, 2, 4, 5]; v = p[v] => v = 0
// u = 1  v = 0 ==> u = 0; result = [0, 1, 2, 4, 5]; v = p[v] => v = null;
// 回溯结束 result = [0, 1, 2, 4, 5];