Open webfansplz opened 4 years ago
Repository (仓库地址):https://github.com/zenorocha/clipboard.js Gain (收获) : iOS复制兼容问题
原复制代码(在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();
官网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
clipboardjs源码包含两个核心文件clipboard.js、clipboard-action.js以及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)
});
}
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)
核心代码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);
}
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';
})
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
);
}
Repository (仓库地址):https://github.com/vuejs/vue Gain (收获) : vue库太大了,所以这次先分享一个mixin的核心代码~ 总结
Mixin其实是有规则设置的,不过使用的比较少,内部也是有设置默认规则和配置项
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}
// 将两个选项对象合并到一个新的对象中。
// 用于实例化和继承的核心实用程序。
export function mergeOptions ( //ts类型检查,vue2是用js,所以通过flow做到类型检查
parent: Object,
child: Object,
vm?: Component
): Object {
// 判断是否是生产环境
if (process.env.NODE_ENV !== 'production') {
// 对组件进行检查
checkComponents(child)
}
// 如果child是function,去他的原型上找到options
if (typeof child === 'function') {
child = child.options
}
// 确保将所有props选项语法标准化为基于对象的形式
normalizeProps(child, vm)
// 将所有注入标准化为基于对象的格式
normalizeInject(child, vm)
// 将原始函数指令规范化为对象格式。
normalizeDirectives(child)
//在子选项上应用扩展和混合
//但前提是它不是原始选项对象
//另一个mergeOptions调用的结果。
//只有合并的选项才具有_base属性
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
// 配置mixin规则,我理解就是调用顺序和是否覆盖
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
分享到各个平台的插件: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;
}
}
},
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];
Share your knowledge and repository sources from Github . ♥️
2020/11/16 - 2020/11/22 ~