Banana-FE / github-weekly

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

2020-Oct-Week4 #3

Open BlingSu opened 3 years ago

BlingSu commented 3 years ago

Share your knowledge and repository sources from Github . ♥️

2020/10/26 - 2020/10/30 ~

BlingSu commented 3 years ago

Repository (仓库地址):https://github.com/vuejs/vue-next/blob/master/packages/reactivity/src/reactive.ts Gain (收获) : 响应式是独立渲染和编译,可以单独使用这个包来做状态管理,简单了解

首先看看响应式目录是做什么的

packages/reactivity/src
|--- baseHandlers.ts        proxy handler,针对 Array,Object常规对象处理的handler
|--- collectionHandlers.ts  同上,只不过针对于 Set, Map, WeakMap, WeakSet 集合数据
|--- computed.ts            计算...
|--- effect.ts              依赖手机和数据响应的处理
|--- index.ts               入口 导出API
|--- lock.ts                锁,大概就是控制 readonly响应变量的 set 和 delete 操作吧
|--- operations.ts          数据类型的枚举
|--- reactive.ts            响应式入口,针对对象
|--- ref.ts                 响应式入口,针对基本类型

稍微熟悉的认都知道,其实响应式系统主要是包括4个关键的API,如下

reactive核心入口

源码里面有很多依赖:

// isObject判断对象, toTypeString获取数据类型
import { isObject, toTypeString } from './@vue/shared'
/**
  proxy 的handlers
  mutableHandlers:可变数据的 handler
  reaonlyHandlers:只读数据的 handler
*/
import { mutableHandlers, readonlyHanlders } from './baseHandlers'
// 可变数据集合 和 只读数据集合
import { mutableCollectionHandlers, readonlyCollectionHanlders } from './collectionHanlders'
// effect 泛型类型
import { ReactiveEffect } from './effect'
// ref 泛型类型
import { UnwrapRef, Ref } from './ref'
// 把Stringn转成Map 返回 fn 来判定是否这个Map上面包含所有的key
import { makeMap } from '@vue/shared'

上面可以看到重点依赖项就是一堆的 handler,其他都是一些工具方法或者泛型类型,可以先看看 reactive的 方法和类型

// 依赖集合类型
export type Dep = Set<ReactiveEffect>

/**
  key 和 Dep集合的对应关系集合
  key就是 响应式的key dep就是哪些地方用到了这个key
  假设 const a = { b: 1 } 如果两个地方用到了 a.b
  那么key就是 b ,dep就是两次的依赖集合
*/
export type KeyToDepMap = Map<string | symbol, Dep>

// 观察者是否被发现
const canObserve = (value: any): boolean

// 解ref类型
type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>

// 响应式入口方法 入参数是 T 继承 对象,返回上面的 解ref类型
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>

// 和 上面一样,但是返回是 只读
export function readonly<T extends object>(
  target: T
): Readonly<UnwrapNestedRefs<T>>

// 创建响应式对象关键方法, reactive和only都调用这玩意儿
function createReactiveObject(
  target: any,
  toProxy: WeakMap<any, any>,
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: Proxyhandler<any>
): any

// 是否响应式数据
export function isReactive(value: any): boolean
// 只读?
export function isReadonly(value: any): boolean

// 响应式转化成原始数据
export function toRaw<T>(observed: T): T

// 把value标记成 readonly, 在reactive里面判断是否是 只读 的原始数据
export function markReadonly<T>(value: T): T
// 把value标记成 不可响应数据 ,会影响到 canobserve 方法
export function markNonReactive<T>(value: T): T

上面代码可能会产生疑惑,比如

  1. 如何判断是否只读的响应式数据?如何在上面加一个标记?还是说用一个对象来存储响应式数据
  2. 如何把响应式数据转化成原始数据?proxy数据咋转成object?

/* 这里无非就是 原始数据 -> 响应式数据 响应式数据 -> 原始数据 原始数据 -> 只读响应式数据 只读响应式数据 -> 原始数据 / const rawToReactive = new WeakMap<any, any>() const reactiveToRaw = new WeakMap<any, any>() const rawToReadonly = new WeakMap<any, any>() const readonlyToRow = new WeakMap<any, any>()

/* markReadonly 和 markNonReactive 用来存储标记的特定数据,这样可以在创建响应式数据时来检测有没有被这两个方法标记过! / const readonlyValues = new WeakSet() const nonReactiveValues = nnew WeakSet()

/* 判断是否是集合类型 ,比如 map set weakmap weakset... 注意集合类型的proxy hanlder 和 普通对象是不一样的 / const isObserveTypes = new Set([Set, Map, WeakMap, WeakSet]) // 然后判断是否是可以观察的类型,在canObserve中 const isObservableType = makeMap( ['Object', 'Array', 'Map', 'Set', 'WeakMap', 'WeakSet'] .map(t => [object ${t}]) .join(',') )


所以说用 `readonlyToRow` 来存储只读响应式数据,比如
```js
export function isReadonly(value: any): boolean {
  return readonlyToRow.has(value)
}

reactiveToRawreadonlyToRow 来存储 响应式数据 -> 原始数据的映射关系,接着获取

export function toRaw<T>(observed: T): T {
  return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed
}

总体而言就是利用各种集合来存储原始数据和响应式数据的映射,这样可以根据这映射关系快速拿到对应的数据。
然后我们看看 canObserve 这里来看看那些数据是可以观测的?

const canObserve = (value: any): boolean => {
  return (
    !value._isValue && // 实例不可观察
    !value._isVNode && //  vdom 不可以
    isObservableType(toTypeString(value)) && // 上面那方法6个不行
    !nonReactiveValues.has(value) //标记不可响应的不行
  )
}

重点 reactivereadonly 核心方法实现

// 返回值就是解 Ref 类型
export function reactive(target: object) {
  if (readonlyToRaw.has(target)) return target //如果是一个只读的响应式数据直接返回
  if (readonlyValues.has(target)) return readonly(target) // 如果被标记了只读,直接调用readonly返回

  // 创建响应式
  return createReactiveObject(
    target, // 原始数据
    rawToReactive, // 原始数据 -> 响应式
    reactiveToRaw, // 反过来
    mutableHandlers,// 可变数据的proxyhandle
    mutableCollectionHandlers // 可变数据结合的proxyhandle
  )

  export function readonly<T extends object>(
    target: T
  ): Readonly<UnWrapNestedRefs<T>> {
    if (reactiveToRaw.has(target)) {
      // 响应数据获取原始数据观察
      target = reactiveToRaw.get(target)
    }

    return createReactiveObject(
      target, // 原始数据
      rawToReadonly, // raw -> readonly observed
      readonlyToRaw, // readonly ovserved -> raw
      readonlyHandlers, // 只读数据的 proxy handler
      readonlyCollectionHandlers // 只读集合数据 proxy handle
    )
  }
}

这里可以看出vue3的核心方法也很简单,逻辑一样,主要是通过对原始数据的检查和转换,核心实现的逻辑其实就在createReactiveObject里面

function createReactiveObject(
  target: any, // 原始数据
  toProxy: WeakMap<any, any>, // raw -> readyonly
  toRaw: WeakMap<any, any>, // readonly observed -> raw
  baseHadnlers: ProxyHandler<any>, // 只读 | 可变 数据proxy handler
  collectionHandlers: ProxyHandler<any> // 只读 | 可变 集合 数据handler
) {
  // 如果不是对象 返回自身, 包括 null, reactive(null)
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // 原始数据如果被观察过,直接raw转observed的映射 ,返回响应式
  let observed = toProxy.get(target)
  if (observed !== void 0) return observed

  // 自身原是响应式 直接返回
  if (toRaw.has(target)) return target
  // 不可观察返回
  if (!canObserved(target)) return target

  // 判断基础数据(object|array) handler 还是集合数据handler
  const hanlders = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  // proxy 创建代理对象,也就是响应式对象
  observed = new Proxy(target, handlers)
  // 创建后 设置好 raw 和 obsrved 的双向映射关系
  toProxy.set(target, observed)
  toRaw.set(observed, target)

  // targetMap就是上面所说的创建默认依赖追踪集合
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}

所以这里可以看看baseHanlder中的代码,首先看依赖

import { reactive, readonly, toRaw } from './reactive'
import { OperationTypes } from './operations' // 操作类型枚举,对于proxy handlers的trap方法
import { track, trigger } from './effect' // 依赖收集和触发依赖回调
import { LOCKED } from './lock' // 锁 用来禁止set和delete
import { isObject, hasOwn, isSymbol } from '@vue/shared' // 判断类型的工具包?
import { isRef } from './ref'

这里 LOCKED的作用是啥??? 猜可能是在组件 mount 和 update 的时候会对组件的 props的代理进行修改,因为我们都知道单向数据流中,子组件内部是不能更改 props 的,但是子组件更新,进行 vnode patch 后需要更新子组件的 props,包括一些动态 props,咱来看看变量和方法概览:

// js内部语言行为描述符集合 比如 symbol.iterator这些
const builtInSymbols = new Set(
  Object.getOwnPropertyNames(Symbol)
    .map(key => (Symbol as any)[key])
    .filter(isSymbol)
)
function createGetter(isReadonly: boolean) {}

function set(
  target: any,
  key: string | symbol,
  value: any,
  receiver: any
): boolean

function deleteProperty(target: any, key: string | symbol): boolean

function has(target: any, key: string | symbol): boolean

function ownKeys(target: any): (string | number | symbol)[]

export const mutableHandlers: ProxyHandler<any> = {
  get: createGetter(false),
  set,
  deleteProperty,
  has,
  ownKeys
}

export const readonlyHandlers: ProxyHandler<any> = {
  get: createGetter(true),

  set(target: any, key: string | symbol, value: any, receiver: any): boolean {
    if (LOCKED) {
      if (__DEV__) {
        console.warn(
          `Set operation on key "${String(key)}" failed: target is readonly.`,
          target
        )
      }
      return true
    } else {
      return set(target, key, value, receiver)
    }
  },

  deleteProperty(target: any, key: string | symbol): boolean {
    if (LOCKED) {
      if (__DEV__) {
        console.warn(
          `Delete operation on key "${String(
            key
          )}" failed: target is readonly.`,
          target
        )
      }
      return true
    } else {
      return deleteProperty(target, key)
    }
  },

  has,
  ownKeys
}

可以看到,mutableHandlers 和 readonlyHandlers 都是定义了 5 个 trap 方法:get、set、deleteProperty、has、ownKeys,前 3 个不用多家介绍,has trap 针对与 in 操作符,而 ownKeys 针对于 for in 和 Object.keys 这些遍历操作的,而 readonlyHandlers 相比于 mutableHandlers 其实只是在 get、set 和 deleteProperty 这三个 trap 方法里有区别,而对于可能改变数据的 set 和 deleteProperty 方法,则是利用 LOCKED 来锁定,不让修改数据,这个变量我在上面也提了一下。

get

// 创建get trap 方法 如果可变 isReadonly 是false 只读的就是true啦
function createGetter(isReadonly: boolean) {
  return function get(
    target: any,
    key: string | symbol,
    receiver: any
  ) {
    // 通过 Reflect 反射来获取原始值
    const res = Reflect.get(target, key, receiver)

    // 如果js内置方法,不依赖收集
    if (isSymbol(key) && builtInSymbols.has(key)) return res

    // 如果是ref数据类型,返回value,可以这么档次ref(target) 其实在 get set value的时候就收集了,所以没必要重复收集
    if (isRef(res)) return res.value
    // get 类型操作的依赖收集
    track(target, OperationTypes.GET, key)
    // 返回响应式。。
    return isObject(res)
      ? isReadonly
        ? readonly(res)
        : reactive(res)
      : res
  }
}

这里用Reflect.get,为什么而不用 target[key]返回? 举个例子

const target = {
  val: 1,
  get v () {
    return this.val
  }
}

const observed = reactive(target)

此时,如果不用 Reflect.get,而是 target[key],那么 this.foo 中的 this 就指向的是 target,而不是 observed,此时 this.foo 就不能收集到 foo 的依赖了,如果 observed.foo = 20 改变了 foo 的值,那么是无法触发依赖回调的,所以需要利用 Reflect.get 将 getter 里的 this 指向代理对象。因为这里的第三个参数recevier就是如果target对象中指定了getter,receiver则为getter调用时的this值!哈哈

还有跟题为什么在结尾 return 的时候还要调用 reactive 或者 readoonly 呢?
因为 proxy的特性就是 只能代理一层,对于嵌套的深层对象,如果不按源码中的方法,那就需要一层层递归来代理劫持对象,即每次递归都判断是否是对象,如果是对象,那么再调用 reactive 来响应式化,js中也很蛋疼,有循环引用的问题,说白了如果一直递归调用 reactive,爆栈就gg了,所以这里要做一些判断如果是toproxy存在不递归不就ok了。所以说相比之下初始化递归劫持,延迟访问劫持的方式更能提升初始化性能,也可以做更细的控制,比如接口数据嵌套深的话,不就很舒服了吗。

const child = { name : 'child' }
const parent = new Proxy({ name: 'parent' }, {
    set (target, key, value, receiver) {
    console.log(target, receiver)
    return Reflect.set(target, key, value, receiver)
    }
})
Object.setPrototypeOf(child, parent)
child.haha = 1
// 结果 { name: 'parent'} { name: 'child'}

仔细看 child的原型链是一个 proxy, child在设置值的时候,本身不包含key。
当满足上面两个条件的时候,设置 child 的值,会触发原型链上的 set trap 方法,并且 target 是原型链数据,而 receiver 则是真实数据所以,源码中的那个条件逻辑也就不难看懂了,当满足上述两个条件时,我们当然不希望触发 parent 的 set trap 咯

像数组的 unshift,splice 这些操作是如何触发 set trap方法的呢?其实就是把数组的每一项依次都往后移动一位,然后再把首位设置成 0

这里可以大概知道以上是做什么的把

scorpioLh commented 3 years ago

Repository (仓库地址):https://github.com/webmodules/jsonp Gain (收获) : 了解jsonp的源码

JSONP简介: Ajax直接请求普通文件存在跨域无权限访问的问题,无论是静态页面、动态网页、web服务、WCF,只要是跨域请求,一律不允许。但是,Web页面上调用js文件时则不受是否跨域的影响。 为了便于客户端使用数据,逐渐形成了一种非正式传输协议,人们把它称作JSONP,该协议的一个要点就是允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。

源码:

/**
   * 加载debug依赖
   */
  var debug = require('debug')('jsonp');

  /**
   * 模块出口
   */
  module.exports = jsonp;

  /**
   * Callback index.
   */
  var count = 0;

  /**
   * 空函数
   */
  function noop(){}

  /**
   * JSONP handler
   *
   * Options:
   *  - param {String} qs parameter (`callback`)
   *  - prefix {String} qs parameter (`__jp`)
   *  - name {String} qs parameter (`prefix` + incr)
   *  - timeout {Number} how long after a timeout error is emitted (`60000`)
   *
   * @param {String} url
   * @param {Object|Function} optional options / callback
   * @param {Function} optional callback
   */
  function jsonp(url, opts, fn){
    // 当第二个参数为function时,默认请求参数为空,将function设置为回调函数fn
    if ('function' == typeof opts) {
      fn = opts;
      opts = {};
    }
    if (!opts) opts = {};

    var prefix = opts.prefix || '__jp';

    // 如果提供了回调名称,则使用传递的回调名称。否则,通过递增计数器生成唯一名称
    var id = opts.name || (prefix + (count++));

    var param = opts.param || 'callback';
    var timeout = null != opts.timeout ? opts.timeout : 60000;
    var enc = encodeURIComponent;
    var target = document.getElementsByTagName('script')[0] || document.head;
    var script;
    var timer;

    // 请求超时,移除script标签,并返回Timeout
    if (timeout) {
      timer = setTimeout(function(){
        cleanup();
        if (fn) fn(new Error('Timeout'));
      }, timeout);
    }

    /** 移除script标签,清空回调函数 */
    function cleanup(){
      if (script.parentNode) script.parentNode.removeChild(script);
      window[id] = noop;
      if (timer) clearTimeout(timer);
    }

    function cancel(){
      if (window[id]) {
        cleanup();
      }
    }

    window[id] = function(data){
      debug('jsonp got', data);
      cleanup();
      if (fn) fn(null, data);
    };

    // 组装请求URL和参数
    url += (~url.indexOf('?') ? '&' : '?') + param + '=' + enc(id);
    url = url.replace('?&', '?');

    debug('jsonp req "%s"', url);

    // 创建script标签,并插入第一个script标签的前面,或者head标签之前
    // insertBefore(newnode,existingnode)。 newnode: 要插入的节点对象, existingnode: 要添加新的节点前的子节点
    script = document.createElement('script');
    script.src = url;
    target.parentNode.insertBefore(script, target);

    return cancel;
  }

使用方式:

import jsonp from 'jsonp'

jsonp(requestUrl, (err, data) => {
  if (!err) {
     resolve(data)
  } else {
     reject(false)
  }
})

完全相同的JSONP请求在并发时如何处理冲突? 造成错误的原因是使用了相同的callback,由于jsonp的callback赋值到window上,并且请求完成后会删除回调方法,所以当两个jsonp请求使用相同的callback时就会造成冲突。 如果每个请求都使用不同的callback是可以解决以上报错问题,但是这会引发服务端性能问题。因为服务端可以通过CDN对相同的请求进行缓存,如果每次callback都不同,服务端缓存就会失效,流向直接冲击源站。所以对用相同参数的jsonp请求callback必须是相同的。如果不同参数或url的jsonp请求使用了相同的callback,我们可以在callback后面加随机数。此操作再jsonp源码中有体现: var id = opts.name || (prefix + (count++)); url += (~url.indexOf('?') ? '&' : '?') + param + '=' + enc(id);

JSONP劫持 这个问题属于CSRF(Cross-site request forgery跨站请求伪造)攻击范畴,当某网站通过JSONP的方式跨域(一般为子域)传递用户认证后的敏感信息时,攻击者可以构造恶意的JSONP调用页面,诱导被攻击者访问,以达到截取用户敏感信息的目的。 关于CSRF可以参考:https://blog.csdn.net/scorpio_h/article/details/94993177?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522160379093619724835838161%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=160379093619724835838161&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_v1~rank_blog_v1-1-94993177.pc_v1_rank_blog_v1&utm_term=CSRF&spm=1018.2118.3001.4187

JSONP劫持的防御

AzTea commented 3 years ago

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

chunk.js:将数组(array)拆分成多个 size 长度的区块,并将这些区块组成一个新数组。 如果array 无法被分割成全部等长的区块,那么最后剩余的元素将组成一个区块。

//Array.slice方法
import slice from './slice.js'
//转化成整型
import toInteger from './toInteger.js'

/**
 * Creates an array of elements split into groups the length of `size`.
 * If `array` can't be split evenly, the final chunk will be the remaining
 * elements.
 *
 * @since 3.0.0
 * @category Array
 * @param {Array} array The array to process.
 * @param {number} [size=1] The length of each chunk
 * @returns {Array} Returns the new array of chunks.
 * @example
 *
 * chunk(['a', 'b', 'c', 'd'], 2)
 * // => [['a', 'b'], ['c', 'd']]
 *
 * chunk(['a', 'b', 'c', 'd'], 3)
 * // => [['a', 'b', 'c'], ['d']]
 */
function chunk(array, size = 1) {
  //size默认1、有传则取传入size和0的最大值
  size = Math.max(toInteger(size), 0)
  //获取数组长度
  const length = array == null ? 0 : array.length
  if (!length || size < 1) {
    return []
  }
  let index = 0 //切数组块的起始索引
  let resIndex = 0 //返回数组的索引
  /**
   * Math.ceil 向上取整
   * Math.round 四舍五入
   * Math.floor向下取整
   */
  const result = new Array(Math.ceil(length / size)) //返回的数组

  //切分
  while (index < length) {
    result[resIndex++] = slice(array, index, (index += size))
  }
  //返回切好的数组
  return result
}

export default chunk

compact.js :创建一个新数组,包含原数组中所有的非假值元素。例如false, null, 0, "", undefined, 和 NaN 都是被认为是“假值”。

/**
 * Creates an array with all falsey values removed. The values `false`, `null`,
 * `0`, `""`, `undefined`, and `NaN` are falsey.
 *
 * @since 0.1.0
 * @category Array
 * @param {Array} array The array to compact.
 * @returns {Array} Returns the new array of filtered values.
 * @example
 *
 * compact([0, 1, false, 2, '', 3])
 * // => [1, 2, 3]
 */
function compact(array) {
  let resIndex = 0
  const result = []

  if (array == null) {
    return result
  }

  for (const value of array) {
    // 判断是真值则加入数组
    if (value) {
      result[resIndex++] = value
    }
  }
  return result
}

export default compact

difference:创建一个具有唯一array值的数组,每个值不包含在其他给定的数组中。使用了SameValueZero做相等比较

相等比较算法:==、===、SameValue(Object.is)、SameValueZero

===和Object.is的区别

  Object.is(NaN, NaN) // true
  Object.is(0, -0) // false

SameValueZero场景(includes内部使用的比较算法就是SameValueZero)

  const s = new Set()
  s.add(0)
  s.add(NaN)
  s.has(-0) // true
  s.has(NaN) // true
wenfeihuazha commented 3 years ago

Repository (仓库地址):https://github.com/vuejs/vue-router Gain (收获) : Vue核心模块Vue-Router的学习整理

Vue-Router在使用前需要进行Vue.use(VueRouter) 在use中的操作

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    // 判断重复安装插件
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
    const args = toArray(arguments, 1)
    // 插入 Vue
    args.unshift(this)
    // 一般插件都会有一个 install 函数
    // 通过该函数让插件可以使用 Vue
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}
export function install (Vue) {
  // 确保 install 调用一次
  if (install.installed && _Vue === Vue) return
  install.installed = true
  // 把 Vue 赋值给全局变量
  _Vue = Vue
  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }
  // 给每个组件的钩子函数混入实现
  // 可以发现在 `beforeCreate` 钩子执行时
  // 会初始化路由
  Vue.mixin({
    beforeCreate () {
      // 判断组件是否存在 router 对象,该对象只在根组件上有
      if (isDef(this.$options.router)) {
        // 根路由设置为自己
        this._routerRoot = this
        this._router = this.$options.router
        // 初始化路由
        this._router.init(this)
        // 很重要,为 _route 属性实现双向绑定
        // 触发组件渲染
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 用于 router-view 层级判断
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })
  // 全局注册组件 router-link 和 router-view
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)
}

手写一个简单的vue-router模块 我的博客地址

webfansplz commented 3 years ago

仓库:

lazyload-Vanilla JavaScript plugin for lazyloading images

源码实现:

/*!
 * Lazy Load - JavaScript plugin for lazy loading images
 *
 * Copyright (c) 2007-2019 Mika Tuupola
 *
 * Licensed under the MIT license:
 *   http://www.opensource.org/licenses/mit-license.php
 *
 * Project home:
 *   https://appelsiini.net/projects/lazyload
 *
 * Version: 2.0.0-rc.2
 *
 */

(function (root, factory) {
  // umd export
  if (typeof exports === "object") {
    module.exports = factory(root);
  } else if (typeof define === "function" && define.amd) {
    define([], factory);
  } else {
    root.LazyLoad = factory(root);
  }
})(
  typeof global !== "undefined" ? global : this.window || this.global,
  function (root) {
    "use strict";

    if (typeof define === "function" && define.amd) {
      root = window;
    }

    const defaults = {
      src: "data-src",
      srcset: "data-srcset",
      selector: ".lazyload",
      root: null,
      rootMargin: "0px",
      threshold: 0,
    };

    /**
     * Merge two or more objects. Returns a new object.
     * @private
     * @param {Boolean}  deep     If true, do a deep (or recursive) merge [optional]
     * @param {Object}   objects  The objects to merge together
     * @returns {Object}          Merged values of defaults and options
     */
    const extend = function () {
      let extended = {};
      let deep = false;
      let i = 0;
      let length = arguments.length;

      /* Check if a deep merge */
      if (Object.prototype.toString.call(arguments[0]) === "[object Boolean]") {
        deep = arguments[0];
        i++;
      }

      /* Merge the object into the extended object */
      let merge = function (obj) {
        for (let prop in obj) {
          if (Object.prototype.hasOwnProperty.call(obj, prop)) {
            /* If deep merge and property is an object, merge properties */
            if (
              deep &&
              Object.prototype.toString.call(obj[prop]) === "[object Object]"
            ) {
              extended[prop] = extend(true, extended[prop], obj[prop]);
            } else {
              extended[prop] = obj[prop];
            }
          }
        }
      };

      /* Loop through each object and conduct a merge */
      for (; i < length; i++) {
        let obj = arguments[i];
        merge(obj);
      }

      return extended;
    };

    function LazyLoad(images, options) {
      // merget default options and options
      this.settings = extend(defaults, options || {});
      this.images = images || document.querySelectorAll(this.settings.selector);
      this.observer = null;
      this.init();
    }

    LazyLoad.prototype = {
      init: function () {
        /* Without observers load everything and bail out early. */
        // 不支持IntersectionObserver API,直接加载图片
        if (!root.IntersectionObserver) {
          this.loadImages();
          return;
        }

        let self = this;
        let observerConfig = {
          root: this.settings.root, // 祖先元素,null或未设置,默认使用顶级文档元素
          rootMargin: this.settings.rootMargin, // 计算交叉时添加到根(root)边界盒bounding box的矩形偏移量
          threshold: [this.settings.threshold], // 阀值(0-1),监听对象的交叉区域与边界区域的比率
        };
        // 使用IntersectionObserver API观察目标元素与root元素的交叉状态
        this.observer = new IntersectionObserver(function (entries) {
          Array.prototype.forEach.call(entries, function (entry) {
            // 目标元素与root元素交叉区域超过threshold阀值
            if (entry.isIntersecting) {
              // 停止监听
              self.observer.unobserve(entry.target);
              // 目前元素 src等属性赋值
              let src = entry.target.getAttribute(self.settings.src);
              let srcset = entry.target.getAttribute(self.settings.srcset);
              // img元素对src赋值,否则对backgroundImage赋值
              if ("img" === entry.target.tagName.toLowerCase()) {
                if (src) {
                  entry.target.src = src;
                }
                if (srcset) {
                  entry.target.srcset = srcset;
                }
              } else {
                entry.target.style.backgroundImage = "url(" + src + ")";
              }
            }
          });
        }, observerConfig);
        Array.prototype.forEach.call(this.images, function (image) {
          // 对目标元素进行监听
          self.observer.observe(image);
        });
      },
      //  加载后销毁
      loadAndDestroy: function () {
        if (!this.settings) {
          return;
        }
        this.loadImages();
        this.destroy();
      },
      // 加载图片
      loadImages: function () {
        if (!this.settings) {
          return;
        }

        let self = this;
        Array.prototype.forEach.call(this.images, function (image) {
          let src = image.getAttribute(self.settings.src);
          let srcset = image.getAttribute(self.settings.srcset);
          if ("img" === image.tagName.toLowerCase()) {
            if (src) {
              image.src = src;
            }
            if (srcset) {
              image.srcset = srcset;
            }
          } else {
            image.style.backgroundImage = "url('" + src + "')";
          }
        });
      },
      // 销毁
      destroy: function () {
        if (!this.settings) {
          return;
        }
        // 解除监听
        this.observer.disconnect();
        this.settings = null;
      },
    };
    // window.lazyload register
    root.lazyload = function (images, options) {
      return new LazyLoad(images, options);
    };
    // jquery plugin register
    if (root.jQuery) {
      const $ = root.jQuery;
      $.fn.lazyload = function (options) {
        options = options || {};
        options.attribute = options.attribute || "data-src";
        new LazyLoad($.makeArray(this), options);
        return this;
      };
    }

    return LazyLoad;
  }
);

收获:

lazyload2.x 版本的核心实现主要是使用了 IntersectionObserver API。

IntersectionObserver 接口提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。可以很简单优雅的判断目标元素是否出现在可视区域内,从而进行处理。

IntersectionObserver API 的兼容性在 IE 全军覆没,但是 w3c 实现了IntersectionObserver polyfill,使得 IE 可以兼容到 7+,真香!

diandiantong commented 3 years ago

网页版 excel 工具:https://github.com/paulhodel/jexcel 收获:组件由直接操作 dom 进行交互,在组件内部直接性存储了一整个表格的节点信息作为每次数据变动操作的依据。 每个方法都分工明确,充分体现了拆分组合方法的精髓。由于初始化值的存在,他可以直接对内部所有信息进行直观性查询。方便监听改动

初始化:判定节点是否可进行挂载

var obj = {};
obj.options = {};
if (! (el instanceof Element || el instanceof HTMLDocument)) {
    console.error('JEXCEL: el is not a valid DOM element');
    return false;
} else if (el.tagName == 'TABLE') {
    if (options = jexcel.createFromTable(el, options)) {
        var div = document.createElement('div');
        el.parentNode.insertBefore(div, el);
        el.remove();
        el = div;
    } else {
        console.error('JEXCEL: el is not a valid DOM element');
        return false;
    }
}

在 defaults 里面设置全部的初始配置(配置太多,就放了,有意去看代码)

加载配置过来的内容,如果存在就替换掉初始化的

for (var property in defaults) {
    if (options && options.hasOwnProperty(property)) {
        if (property === 'text') {
            obj.options[property] = defaults[property];
            for (var textKey in options[property]) {
                if (options[property].hasOwnProperty(textKey)){
                    obj.options[property][textKey] = options[property][textKey];
                }
            }
        } else {
            obj.options[property] = options[property];
        }
    } else {
        obj.options[property] = defaults[property];
    }
}

obj.prepareTable 方法清洗传入数据,然后 使用 obj.createTable 方法进行基础展示表格构建 全程通过 obj.setData(); 方法设置更新数据

为表内组件提供远程支持

obj.refresh = function() {
    if (obj.options.url) {
        // Loading
        if (obj.options.loadingSpin == true) {
            jSuites.loading.show();
        }

        jSuites.ajax({
            url: obj.options.url,
            method: 'GET',
            dataType: 'json',
            success: function(result) {
                // Data
                obj.options.data = (result.data) ? result.data : result;
                // Prepare table
                obj.setData();
                // Hide spin
                if (obj.options.loadingSpin == true) {
                    jSuites.loading.hide();
                }
            }
        });
    } else {
        obj.setData();
    }
}

获取数据为便利他的实例存储内容输出  ``` for (var j = 0; j < y; j++) {     px = 0;     for (var i = 0; i < x; i++) {         // Cell selected or fullset         if (! highlighted || obj.records[j][i].classList.contains('highlight')) {             // Get value             if (! dataset[py]) {                 dataset[py] = [];             }             if (! dataType) {                 dataset[py][px] = obj.records[j][i].innerHTML;             } else {                 dataset[py][px] = obj.options.data[j][i];             }             px++;         }     }     if (px > 0) {         py++;     } }

删除合并:obj.removeMerge

更新表格方式,通过细化更新数据坐标来进行

obj.updateSelection = function(el1, el2, origin) {     var x1 = el1.getAttribute('data-x');     var y1 = el1.getAttribute('data-y');     if (el2) {         var x2 = el2.getAttribute('data-x');         var y2 = el2.getAttribute('data-y');     } else {         var x2 = x1;         var y2 = y1;     }

    obj.updateSelectionFromCoords(x1, y1, x2, y2, origin); }

updateSelectionFromCoords 方法为最终表格数据样式更新

黏贴内容:
1、先解析数据 parseCSV

var data = obj.parseCSV(data, "\t"); // 通过遍历解析出需要的结构 while (row = data[j]) {     i = 0;     colIndex = parseInt(x);

    while (row[i] != null) {         // Update and keep history         var record = obj.updateCell(colIndex, rowIndex, row[i]);         // Keep history         records.push(record);         // Update all formulas in the chain         obj.updateFormulaChain(colIndex, rowIndex, records);         // Style         if (style && style[styleIndex]) {             var columnName = jexcel.getColumnNameFromId([colIndex, rowIndex]);             newStyle[columnName] = style[styleIndex];             oldStyle[columnName] = obj.getStyle(columnName);             obj.records[rowIndex][colIndex].setAttribute('style', style[styleIndex]);             styleIndex++         }         i++;         if (row[i] != null) {             if (colIndex >= obj.headers.length - 1) {                 obj.insertColumn();             }             colIndex = obj.right.get(colIndex, rowIndex);         }     }

    j++;     if (data[j]) {         if (rowIndex >= obj.rows.length-1) {             obj.insertRow();         }         rowIndex = obj.down.get(x, rowIndex);     } }



其他还有很多功能都可以通过文档去找到对应方法,此库学到的主要是功能解耦组合。
每种功能对应一个或一个组合方法, 通过方法组合来实现多样化功能复用。

CSV解析
`obj.parseCSV = function(str, delimiter) {
    // Remove last line break
    str = str.replace(/\r?\n$|\r$|\n$/g, "");
    // Last caracter is the delimiter
    if (str.charCodeAt(str.length-1) == 9) {
        str += "\0";
    }
    // user-supplied delimeter or default comma
    delimiter = (delimiter || ",");

    var arr = [];
    var quote = false;  // true means we're inside a quoted field
    // iterate over each character, keep track of current row and column (of the returned array)
    for (var row = 0, col = 0, c = 0; c < str.length; c++) {
        var cc = str[c], nc = str[c+1];
        arr[row] = arr[row] || [];
        arr[row][col] = arr[row][col] || '';

        // If the current character is a quotation mark, and we're inside a quoted field, and the next character is also a quotation mark, add a quotation mark to the current column and skip the next character
        if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; }  

        // If it's just one quotation mark, begin/end quoted field
        if (cc == '"') { quote = !quote; continue; }

        // If it's a comma and we're not in a quoted field, move on to the next column
        if (cc == delimiter && !quote) { ++col; continue; }

        // If it's a newline (CRLF) and we're not in a quoted field, skip the next character and move on to the next row and move to column 0 of that new row
        if (cc == '\r' && nc == '\n' && !quote) { ++row; col = 0; ++c; continue; }

        // If it's a newline (LF or CR) and we're not in a quoted field, move on to the next row and move to column 0 of that new row
        if (cc == '\n' && !quote) { ++row; col = 0; continue; }
        if (cc == '\r' && !quote) { ++row; col = 0; continue; }

        // Otherwise, append the current character to the current column
        arr[row][col] += cc;
    }
    return arr;
}`

更多更新在这里 http://note.youdao.com/s/QE87r3hZ
rxyshww commented 3 years ago

Repository (仓库地址):vue-clamp Gain (收获) : js实现超出时显示省略号(或其他字符)getClientRects的使用

可能用到的地方:

1、css不好使的地方:用html2Canvas时 2、想用别的字符代替...时 3、H5中,用rem时可能出现的问题,例如 height是line-height的二倍,转成浏览器转成像素编程 height:40px line-height:19px,然后第三行露出。(当然也可以通过稍微增大一点点line-height解决,如果UI不是像素眼)。

核心代码

mounted () {
    this.init()

    // 对一些变量监听,改变后重新渲染
    this.$watch(
      (vm) => [vm.maxLines, vm.maxHeight, vm.ellipsis, vm.isClamped].join(),
      this.update
    )
    this.$watch((vm) => [vm.tag, vm.text, vm.autoresize].join(), this.init)
  }

// 在页面中渲染 this.realText
render (h) {
    const contents = [
      h(
        'span',
        this.$isServer
          ? {}
          : {
            ref: 'text',
            attrs: {
              'aria-label': this.text.trim()
            }
          },
        this.$isServer ? this.text : this.realText
      )
    ]
}

computed: {
    clampedText () {
      return this.text.slice(0, this.offset) + this.ellipsis
    },
    isClamped () {
      if (!this.text) {
        return false
      }
      return this.offset !== this.text.length
    },
    realText () {
      return this.isClamped ? this.clampedText : this.text
    }
}

methods: {
    // 判断是否超出,true则为需要展示省略号
    isOverflow () {
      if (!this.maxLines && !this.maxHeight) {
        return false
      }

      if (this.maxLines) {
        if (this.getLines() > this.maxLines) {
          return true
        }
      }

      if (this.maxHeight) {
        if (this.$el.scrollHeight > this.$el.offsetHeight) {
          return true
        }
      }
      return false
    },
    update () {
      if (this.localExpanded) {
        return
      }
      this.applyChange()
      if (this.isOverflow() || this.isClamped) {
        this.search()
      }
    }
    search (...range) {
      const [from = 0, to = this.offset] = range
      if (to - from <= 3) {
        this.stepToFit()
        return
      }
      const target = Math.floor((to + from) / 2)
      this.clampAt(target)
      if (this.isOverflow()) {
        this.search(from, target)
      } else {
        this.search(target, to)
      }
    },
    clampAt (offset) {
      this.offset = offset
      this.applyChange()
    },
    applyChange () {
      this.$refs.text.textContent = this.realText
    }

}

我们来梳理一下流程: 1、render生成基本的dom 2、mounted 中 watch 监听依赖 改变则执行update(init本质也是update,只是多了一些初始化方法) 3、mounted 调用 init,init调用update, update调用search 4、search方法是一个递归的过程,递归过程中,会一直增加this.offset这个值,从而使计算属性clampedText(页面最终展示的文本)增加,直到this.isOverflow()为true,这时候再去减少this.offset,直到to - from < 3结束递归,从而实现加上...正好占满最后一行

isOverflow依赖两个值:maxLines和maxHeight,看代码发现,这两个得至少配置一个。意为最多显示几行,或超过多少高度时隐藏。 1、maxLines的判断:

    if (this.getLines() > this.maxLines) {
         return true
    }

    getLines () {
      return Object.keys(
        [...this.$refs.content.getClientRects()].reduce(
          (prev, { top, bottom }) => {
            const key = `${top}/${bottom}`
            if (!prev[key]) {
              prev[key] = true
            }
            return prev
          },
          {}
        )
      ).length
    }

getClientRects返回值是ClientRect对象集合,该对象是与该元素相关的CSS边框。每个ClientRect对象包含一组描述该边框的只读属性——left、top、right和bottom,单位为像素,这些属性值是相对于视口的top-left的。

上边的话是官网上查的,多读几遍也还是不知道在讲什么,我们通过一个栗子来理解:

<span id="span" style="width: 100px">范德萨发史蒂夫范德萨发史蒂夫范德萨发史蒂夫范德萨发史蒂夫范德萨发</span>

1604244003974

1604244097723

现在发现了,他就是获取当前dom中每个元素块的信息,从而了解到一共有多少行,prev[key]可以剔除一行有两个元素块的情况。

2、maxHeight的判断: 这个比较简单

this.$el.scrollHeight > this.$el.offsetHeight