欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

顺带学TS语言,一起深入解读BetterScroll的源代码

最编程 2024-07-23 08:39:35
...

开头


TypeScript已经出来很多年了,现在用的人也越来越多,毋庸置疑,它会越来越流行,但是我还没有用过,因为首先是项目上不用,其次是我对强类型并不敏感,所以纯粹的光看文档看不了几分钟就心不在焉,一直就被耽搁了。


但是,现在很多流行的框架都开始用TypeScript重构,很多文章的示例代码也变成TypeScript,所以这就很尴尬了,你不会就看不懂,所以好了,没得选了。


既然目前我的痛点是看源码看不懂,那不如就在看源码的过程中遇到不懂的TypeScript语法再去详细了解,这样可能比单纯看文档更有效,接下来我将在阅读BetterScroll源码的同时恶补TypeScript


BetterScroll是一个针对移动端的滚动库,使用纯JavaScript,2.0版本使用TypeScript进行了重构,通过插件化将功能进行了分离,核心只保留基本的滚动功能。


方便起见,后续TypeScript缩写为TSBetterScroll缩写为BS

BS的核心功能代码在/packages/core/文件夹下,结构如下:


image.png


index.ts文件只用来对外暴露接口,我们从BScroll.ts开始阅读。



入口类


interface PluginCtor {
  pluginName: string
  applyOrder?: ApplyOrder
  new (scroll: BScroll): any
}


interface接口用来定义值的结构,之后TS的类型检查器就会对值进行检查,上面的PluginCtor接口用来对BS的插件对象结构进行定义及限制,意思为需要一个必填的字符串类型插件名称pluginName?的意思为可选,可有可不有的ApplyOrder类型的调用位置,找到ApplyOrder的定义:


export const enum ApplyOrder {
  Pre = 'pre',
  Post = 'post'
}


enum的意思是枚举,可以定义一些带名字的常量,使用枚举可以清晰的知道可选的选项是什么,枚举支持数字枚举和字符串枚举,数字枚举还有自增的功能,上述通过const来修饰的枚举称为常量枚举,常量枚举的特点是在编译阶段会被删除而直接内联到使用的地方。


回到接口,interface可以为类和实例来定义接口,这里有个new意味着这是为类定义的接口,这里我们就可以知道BS的插件主体需要是一个类,且有两个静态属性,构造函数入参是BS的实例,any代表任何类型。


再往下:


interface PluginsMap {
  [key: string]: boolean
}


这里同样是个接口定义,[key: string]的属性称作索引签名,因为TS会对对象字面量进行额外属性检查,即出现了接口里没有定义的属性时会认为是个错误,解决这个问题的其中一个方法就是在接口定义里增加索引签名。


type ElementParam = HTMLElement | string


type意为类型别名,相当于给一个类型起了一个别名,不会新建类型,是一种引用关系,使用的时候和接口差不多,但是有一些细微差别。


|代表联合类型,表示一个值可以是几种类型之一。


export interface MountedBScrollHTMLElement extends HTMLElement {
  isBScrollContainer?: boolean
}


接口是可以继承的,继承能从一个接口里复制成员到另一个接口里,增加可重用性。


export class BScrollConstructor<O = {}> extends EventEmitter {}


<o = {}><>称为泛型,即可以支持多种类型,不限制为具体的一种,为扩展提供了可能,也比使用any严谨,<>就像()一样,调用的时候传入类型,<>里的参数来接收,<>里的参数称为类型变量,比如下面的泛型函数:


function fn<T>(arg: T): T {}
fn<Number>(1)


表示入参和返回参数的类型都是Number,除了<>,入参里的T和返回参数类型的T可以理解为是占位符。


static plugins: PluginItem[] = []


[]代表数组类型,定义数组有两种方式:


let list: number[] = [1,2,3]// 1.元素类型后面跟上[]
let list: Array<number> = [1,2,3]// 2.使用数组泛型,Array<元素类型>


所以上面的意思是定义了一个元素类型是PluginItem的数组。


BS使用插件需要在new BS之前调用use方法,useBS类的一个静态方法:


class BS {
    static use(ctor: PluginCtor) {
        const name = ctor.pluginName
        // 插件名称检查、插件是否已经注册检查...
        BScrollConstructor.pluginsMap[name] = true
        BScrollConstructor.plugins.push({
          name,
          applyOrder: ctor.applyOrder,
          ctor,
        })
        return BScrollConstructor
      }
}


use方法就是简单的把插件添加到plugins数组里。


class BS {
    constructor(el: ElementParam, options?: Options & O) {
        super([
            //注册的事件名称
        ])
      const wrapper = getElement(el)// 获取元素
      this.options = new OptionsConstructor().merge(options).process()// 参数合并
        if (!this.setContent(wrapper).valid) {
          return
        }
        this.hooks = new EventEmitter([
          // 注册的钩子名称
        ])
      this.init(wrapper)
    }
}


构造函数做的事情是注册事件,获取元素,参数合并处理,参数处理里进行了环境检测及浏览器兼容工作,以及进行初始化。BS本身继承了事件对象,实例派发的叫事件,这里又创建了一个事件对象的实例hooks,在BS里为了区分叫做钩子,普通用户更关注事件,而插件开发一般要更关注钩子。


setContent函数的作用是设置BS要处理滚动的content,BS默认是将wrapper的第一个子元素作为content`,也可以通过配置参数来指定。


class BS {
    private init(wrapper: MountedBScrollHTMLElement) {
        this.wrapper = wrapper
        // 创建一个滚动实例
        this.scroller = new Scroller(wrapper, this.content, this.options)
        // 事件转发
        this.eventBubbling()
        // 自动失焦
        this.handleAutoBlur()
        // 启用BS,并派发对应事件
        this.enable()
        // 属性和方法代理
        this.proxy(propertiesConfig)
        // 实例化插件,遍历BS类的plugins数组挨个进行实例化,并将插件实例以key:插件名,value:插件实例保存到BS实例的plugins对象上
        this.applyPlugins()
        // 调用scroller实例刷新方法,并派发刷新事件
        this.refreshWithoutReset(this.content)
        // 下面的用来设置初始滚动的位置
        const { startX, startY } = this.options
        const position = {
          x: startX,
          y: startY,
        }
        if (
          // 如果你的插件要修改初始滚动位置,那么可以监听这个事件
          this.hooks.trigger(this.hooks.eventTypes.beforeInitialScrollTo, position)
        ) {
          return
        }
        this.scroller.scrollTo(position.x, position.y)
      }
}


init方法里做了很多事情,一一来看:


{
    private eventBubbling() {
        bubbling(this.scroller.hooks, this, [
          this.eventTypes.beforeScrollStart,
          // 事件...
        ])
      }
}
// 事件转发
export function bubbling(source,target,events) {
  events.forEach(event => {
    let sourceEvent
    let targetEvent
    if (typeof event === 'string') {
      sourceEvent = targetEvent = event
    } else {
      sourceEvent = event.source
      targetEvent = event.target
    }
    source.on(sourceEvent, function(...args: any[]) {
      return target.trigger(targetEvent, ...args)
    })
  })
}


BS实例的构造函数里注册了一系列事件,有些是scroller实例派发的,所以需要监听scroller对应的事件来派发自己注册的事件,相当于事件转发。


{
    private handleAutoBlur() {
        if (this.options.autoBlur) {
          this.on(this.eventTypes.beforeScrollStart, () => {
            let activeElement = document.activeElement as HTMLElement
            if (
              activeElement &&
              (activeElement.tagName === 'INPUT' ||
                activeElement.tagName === 'TEXTAREA')
            ) {
              activeElement.blur()
            }
          })
        }
      }
}


配置项里有一个参数:autoBlur,如果设为true会监听即将滚动的事件来将当前页面上激活的元素(input、textarea)失去焦点,document.activeElement可以获取文档中当前获得焦点的元素。


另外这里出现了asTS支持的数据类型有:boolean、number、string、T[]|Array、元组、枚举enum、任意any、空void、undefined、null、永不存在的值的类型never、非原始类型object,有时候你会确切的知道某个值是什么类型,可能会比TS更准确,那么可以通过as来指明它的类型,这称作类型断言,这样TS就不再进行判断了。


{
    proxy(propertiesConfig: PropertyConfig[]) {
        propertiesConfig.forEach(({ key, sourceKey }) => {
          propertiesProxy(this, sourceKey, key)
        })
      }
}


插件会有一些自己的属性和方法,proxy方法用来代理到BS实例,这样可以直接通过BS的实例访问,propertiesConfig的定义如下:


export const propertiesConfig = [
  {
    sourceKey: 'scroller.scrollBehaviorX.currentPos',
    key: 'x'
  },
  // 其他属性和方法...
]


export function propertiesProxy(target,sourceKey,key) {
  sharedPropertyDefinition.get = function proxyGetter() {
    return getProperty(this, sourceKey)
  }
  sharedPropertyDefinition.set = function proxySetter(val) {
    setProperty(this, sourceKey, val)
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}


通过defineProperty来定义属性,需要注意的是sourceKey的格式都是需要能让BS的实例this通过.能访问到源属性才行,比如这里的

this.scroller.scrollBehaviorX.currentPos可以访问到scroller实例的currentPos属性,如果是一个插件的话,你的propertiesConfig需要这样:


{
    sourceKey: 'plugins.myPlugin.xxx',
    key: 'xxx'
  }


pluginsBS实例上的一个属性,这样通过this.plugins.myPlugin.xxx就能访问到你的源属性,也就能够直接通过this修改到源属性的属性值。所以setPropertygetProperty的逻辑也就很简单了:


上一篇: 锁定安卓手机屏幕方向: 为Android应用设置定向模式

下一篇: 深入理解Android应用架构:Activity组件详细解析