Coder Social home page Coder Social logo

blog's People

Contributors

impeiran avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

blog's Issues

模拟实现instanceof并谈其原理

instanceof用来判断左侧变量是否为右边变量的实例,而创造一个实例的new关键字,其实就是根据构造函数的原型属性prototype创造新的对象,并以其为上下文执行构造函数。

因此,对于实现instanceof,我们可以获取左侧变量的构造函数的原型对象,跟右侧变量的原型对象进行比较。

但这样只是获取原型链上目标对象的直接父级,还需要逐层递归往原型链上面进行查找。

JS的对象里就有一个属性__proto__,指向的就是当前对象的构造函数的原型,我们可以基于此实现instanceof

/**
 * mock instanceof
 * @param {Any} left 
 * @param {Any} right 
 */
const _instanceof = (left, right) => {
  if (!left || !right) return new Error('lack of parameter')

  let proto = left.__proto__

  right = right.prototype || {}

  while (proto) {
    if (proto === right) {
      return true
    } else {
      proto = proto.__proto__
    }
  }

  return false
}

vue2.x 响应式原理及依赖收集的简单实现

本文将把vue 2.x的响应式原理及其依赖收集进行分析讲解,并简单用代码模拟一遍vue这一部分的源码。

点击查看模式实现的代码

(此处及之后的vue泛指2.x版本的vue)

绝大部分人都知道vue2.x的响应式依赖一个APIObject.defineProperty,通过该API可以劫持属性的获取(get)/赋值(set),在回调中进行更新视图,从而达到由数据驱动视图去更新。

但要实现响应式,细节上还需要更多,譬如:

  • 属性被set之后,具备什么样的条件才能更新视图。

  • 上述第一点满足后,框架如何知道具体要更新哪一个视图组件。

  • 如何解决多个属性触发同一个组件更新的情况。

  • ...

抛出上述问题之后,vue的做法是这样的:

  1. 引入依赖收集机制:递归遍历组件状态data()之后,每个属性key作为一个依赖,实例化一个名为Dep的依赖对象const dep = new Dep(),并用Object.defineProperty劫持get/set
    • 视图渲染过程中触发属性getter,在getter的回调中收集其对应的依赖dep
    • 主动set属性时,在属性的setter回调中,其对应dep通知所有收集到它的对象。
  2. 设立一个对象Watcher,进行上述依赖的收集和管理。该watcher对应到每一个组件,Vue把其称为render watcher。属性被set之后,依赖对象dep就会通知将它收集的watcher,由watcher进行更新视图。

引用vue官方的一张图展示这一个过程:

data

首先组件render的时候,渲染在视图中的状态都会触发其getter,然后组件对应的Watchergetter回调中将其作为依赖进行收集。当状态发生变化后,通知notify收集其依赖的Watcher,然后Watcher进行更新,触发组件的重新render。

接下来由代码来讲述这整个流程,基本就是vue源代码的简化版,省略了大部分的变量校验和与本文主题无关的代码。想仔细研究完整版本请自行查阅源码。

状态属性的getter/setter

首先我们要拿到组件中声明的状态,默认规定是一个工厂函数,返回一个object,这样就可以保证每次根据配置实例化的状态,不会指向同一份状态。拿到状态data后,我们就进行观察劫持。

// 获取data 并保留一份指引在实例上,即this._data
const data = vm._data = vm.$option.data.call(vm)

// 将状态代理到实例上,就可以通过this.xxx获取
// 源码中是将 this.xxx 代理到 this._data.xxx
const keys = Object.keys(data)
let i = keys.length
while (i--) {
  const key = keys[i]
  proxy(this, '_data', key)
}

// 然后进行递归的observe
observe(data)

下面则是进行代理的proxy方法实现

// 将状态代理到目标上
function proxy (source, sourceKey, k) {
	Object.defineProperty(source, k, {
    enumerable: true,
    configurable: true,
    get: function () {
      return this[sourceKey][k]
    },
    set: function (val) {
      this[sourceKey][k] = val
    }
  })
}

接着讲解的是observe(data),这里要完成的就是递归进行劫持。源码中整个流程:observe(data)->new Observer(data)->walk(data)->defineReative(data, key)。当中涉及到数组和普通对象的处理,以及是否需要劫持的判断,故在此不作展开。简化版如下,并不影响我们的逻辑分析:

function observe (obj) {
  const keys = Object.keys(obj)
  for (const key of keys) {
    const dep = new Dep()
		// 保存对象-Key的取值
    let val = obj[key]
		
    // 	递归劫持
    if (Object.prototype.toString.call(val) === 'object') {
      observe(val)
    }

    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function () {
        // 进行依赖收集
        if (Dep.target) {
          dep.depend()
        }
        return val
      },

      set: function (newVal) {
        // vue此处做了优化,如果值没变化,则不会通知watcher
        if (newVal === val) return
        // 变化之后需要再次赋值
        val = newVal
        // 由依赖进行通知
        dep.notify()
      }
    })
  }
}

依赖 Dep

这是递归遍历data时,为每一个key值实例化的类,一个key对应一个dep。首先看下Dep类的简单实现:

let depId = 0
class Dep {
  // 静态属性,类型是Watcher
  static target;
  
  constructor () {
    this.id = depId++
    this.subs = []
  }
	
  // 添加订阅者,当属性发生变化,可以透过属性去查找其对应的watcher
  addSub (sub) {
    this.subs.push(sub)
  }
  
  // 移除订阅
  removeSub (sub) {
    const index = this.subs.findIndex(sub)
    if (index !== -1) {
      this.subs.splice(index, 1)
    }
  }
	// 依赖收集
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
	
  // 广播更新
  notify () {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
Dep.target = null

有两个设计点:

  1. 内部维护了一个订阅数组,一个属性不仅可以被render watcher收集,也可以被user watcher收集,即用户自己编写的watch选项,等等。
  2. 维护了一个静态属性target,存放当前进行renderwatcher。虽然说vue的更新是异步的,但是这个异步只是相对于改变状态的操作而言,对于模版/render方法渲染单个组件的过程依然是js同步进行的。所以全局同一时候只会有一个watcher进行更新,更新完当前的watcher,再将新的watcher重新赋值到Dep.target

vue使用了一个栈来维护当前的Dep.target,因为考虑到当前watcher更新时,可能会触发另一个watcher的更新渲染,需要对上一个watcher进行保留。

const targetStack = []

function pushTarget (target) {
  targetStack.push(target)
  Dep.target = target
}

function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

而什么时候需要赋值当前的Dep.target,就在于Watcher的设计了。

Watcher

vue里面的Watcher,负责了三个功能:computed、用户自定义watcherdata状态的依赖收集,而前两者都有依赖于第三个依赖收集的机制。源码里的Watcher考虑到了更多的情景,这里只针对依赖收集的那一部分代码进行简单实现:

let watchId = 0
class Watcher {
  constructor (vm, expOrFn) {
    this.id = watchId++
    this.vm = vm
    // 这里是获取值的回调,也可以穿入render/update方法
    this.getter = expOrFn		

    // 用于处理依赖
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
		
    // 实例化时,会执行一遍“获取”的get函数
    this.value = this.get()
  }
	
  // 收集依赖
  addDep (dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }
	
  // 清理依赖
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }
	// 获取watcher渲染的值,如果是render watcher,其返回的值不必用到,只会执行逻辑上的渲染
  get () {
    pushTarget(this)
    const value = this.getter.call(this.vm)
    popTarget()

    return value
  }
	// 这里源码里是会启动一个异步队列,进行更新
  update () {
    Promise.resolve().then(() => {
      this.get()
    })
  }
}

watcher的实例化时机就在所有状态、事件、注入都初始化完了之后,DOM进行mount之前。那一刻data中的状态均已完成了响应式劫持的声明。

// 源码中大概的调用:
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

对照回往上watcher的声明,可以发现其实render watcher里,expOrFn函数就会被传递成一个render的函数。代表着,实例化watcher代码的最后执行this.get(),相当于都会执行一次传递过来的render

紧接着我们来看get函数

get () {
  pushTarget(this)
  const value = this.getter.call(this.vm)
  popTarget()

  return value
}

在这里,就把当前正在渲染watcher变成Dep.target,然后执行参数中传递过来的render,在render过程中就会触发data状态的get属性回调,并执行依赖收集。

addDep (dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}

依赖收集这里用了两对数组,一对是new开头的,分别存放id和实例的。另一对则不带new,意味着是原来的。

这样设计的原因是**每一次的render,模板中用到的状态可能会不一样。**e.g:v-if的状态由true改变成了false,并且v-if下的代码块中包含了声明的状态date。那么对比前后,第一次渲染的时候watcher收集到了date的依赖,但是状态改变之后,date的状态被v-if="false"包裹了,对于视图来说我们不需要收集这个依赖去更新了。

所以每一次更新我们都要重新清除cleanupDeps上一次收集过的依赖,赋值给新的依赖。就可以避免改变视图中没用到的状态,也会触发更新这个场景,可以说是一种优化。

实践 && 测试

结合了上述编写的简单DepWatcher,简单写一个Vue类试验一下。

class Vue {
  constructor (option) {
    this._option = option
    this._el = document.querySelector(option.el)
    this._template = this._el.innerHTML

    this._initState(this)

    new Watcher(this, function update () {
      this._mount()
    })
  }
	
  // 递归遍历data 初始化响应式
  _initState () {
    const data = this._data = this._option.data
      ? this._option.data() : {}

    const keys = Object.keys(data)
    let i = keys.length
    while (i--) {
      const key = keys[i]
      proxy(this, '_data', key)
    }
    observe(data)
  }

  _mount () {
    const _this = this
    let template = _this._template

    // 替换差值表达式
    let matchText
    while ((matchText = /\{\{((\w)+?)\}\}/.exec(template))) {
      template = template.replace(matchText[0], _this._data[matchText[1]])
    }

    _this._el.innerHTML = template
  }
}

这里没有模拟virtual-dom,只模拟vue中响应式和依赖收集的场景。然后html中编写以下代码:

<body>
  <div id="app">
    <div>计数器:{{counter}}</div>
    <div>当前时间戳:{{currentDate}}</div>
  </div>

  <button id="counter">增加</button>
  <button id="timer">打印时间</button>
</body>

<script>
const app = new Vue({
  el: '#app',
  data () {
    return {
      counter: 1,
      currentDate: Date.now()
    }
  }
})

const $ = sel => document.querySelector(sel)

$('#counter').onclick = () => {
  app.counter++
}

$('#timer').onclick = () => {
  app.currentDate = Date.now()
}
</script>

试验也能成功

浏览器输入URL发生了什么

比较考验理念知识积累,后续笔者会随着经验增加再依次补充。

分析协议

首先第一步,如果URL是File://协议开头,浏览器首先会查找机子上的本地文件。如果是FTP就会按照其对应规则建立连接。如果是http/https则会按照下面步骤进行。

域名查找解析

如果地址是直接使用ip时,则跳过域名解析阶段。

地址是使用域名型的,就会进行此步骤,涉及到的概念:

  • 根 DNS 服务器: 返回顶级域DNS服务器的IP地址
  • 顶级域DNS服务器:返回权威DNS服务器的IP地址
  • 权威DNS服务器: 返回相应主机的IP地址

整个查找解析是一个递归过程:首先从

1 客户端/浏览器缓存中查找 ->
2 本地的hosts文件查找 ->
3 本地DNS解析器(此时可能命中缓存) ->
4 本地DNS服务器 ->

当上述这个过程都未命中时,根据本地DNS服务器设置的转发器进行查询。若未用转发模式,则迭代查找过程如下:

5 根域名服务器 ->
6 顶级域名服务器 ->
7 权威域名服务器

缓存与优化

  • DNS存在着多级缓存,从离浏览器的距离排序的话,有以下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。
  • 拓展httpDNS,服务商可自定义匹配IP的规则,适合做分布式缓存与负载均衡。
  • 在域名和 IP 的映射过程中,给了应用基于域名做负载均衡的机会,可以是简单的负载均衡,也可以根据地址和运营商做全局的负载均衡。

建立tcp链接

如果是http(s)的链接,还是基于tcp的,需要建立起链接。涉及到TCP的三次握手,主要作用是为了确认双方的接收能力和发送能力是否都正确,指定自己的初始化序列号为后面的可靠性传送做准备。

涉及概念:

  • SYN(SYNchronization):在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文。对方若同意建立连接,则应在响应报文中使SYN=1和ACK=1. 因此, SYN置1就表示这是一个连接请求或连接接受报文。
  • ACK:此标志表示应答域有效,就是说前面所说的TCP应答号将会包含在TCP数据包中;有两个取值:0和1,为1的时候表示应答域有效,反之为0。TCP协议规定,只有ACK=1时有效,也规定连接建立后所有发送的报文的ACK必须为1。
  • FIN:即完,终结的意思, 用来释放一个连接。当 FIN = 1 时,表明此报文段的发送方的数据已经发送完毕,并要求释放连接。
  • seq:序号,本报文段发送的数据的第一个字节的序号。
  • ack:期望收到对方下一个报文段的第一个数据字节的序号。

一开始:客户端处于Closed状态,服务端处于Listen状态。

第一次握手

发起方A向B发起连接请求报文段,tcp首部的控制位SYN设为1,序号seq=x(某个开始的传输字节数)。此时SYN=1时不携带数据,但要消耗一个序号位。此时发起方进入SYN-SENT(同步已发送)阶段。

第二次握手

接收方B收到请求连接报文,若同意建立连接,则发送报文段:设控制位ACK=1SYN继续为1,确认序号ack设为x+1,同时为自己的序号位seq=y。此时该报文依然不携带数据,TCP的服务器进程进入SYN-RCVN(同步收到状态)。

第三次握手

发送方A接收到确认报文后,还要再发送一次确认:控制位ACK=1,序号seq仍然为x+1,确认号ack为y+1。A发送后进入ESTABLISHED(已建立连接)状态,B收到确认后也进入ESTABLISH状态。

PS:如果不携带数据就不消耗序号位,即上次序号位为a且不带数据,下一次仍可继续发送序号位为a的报文。

延伸:为什么需要第三次握手,而不是两次握手?

因为是为了防止A第一次握手时发送的报文,被某些因素导致延误了,而此时A因为超时,重新建立了连接,当延误的报文到了之后,B继续确认进行第二次握手,重复建立链接。

有了第三次握手,则会确保第一次延误时不会进行多次重复的连接。

TSL/SSL握手过程(若有)

TLS以SSL3.0为基准,后又制定了TLS1.0、TLS1.1和TLS1.2。当前主流的版本是SSL3.0和TLS1.0。

整个握手过程采用非对称加密与对称加密混合的方式。因为全部使用非对称加密的方式,算法实现会很耗时间,整个传输过程就会变得效率低。全部采用对称加密的方式,一开始若密钥被监听到,则就不安全。

所以正常数据传输时采用对称加密,而对称加密的密钥,采用安全性更好的非对称加密进行传输。

第一次握手 - Client Hello

客户端首先要告诉服务端,自己可以支持哪些加密算法。于是客户端把本地支持的加密套件(cipher suites),以及产生一个随机数(Client Random),两者一并传输给服务端。而且,客户端也要保存这个随机数。

第二次握手 - Server Hello

服务端收到信息后,保存第一次的随机数。然后把证书(包含公钥、数字签名、过期信息、其余网站信息),以及生成并保存第二个随机数(Server Random),并确定使用的加密方法,三个信息一并传输回给客户端(server hello done)。

若此次发送的证书信息不齐全时,中间还会发送一个Server Key Exchange信息,补充回给客户端。

第三次 - Client Key Exchange

接着,客户端收到证书及信息后。首先验证证书的完整性、正确性、以及证书上的域名与服务端域名是否一致。

拓展数字签名:证书的数字签名由CA相关组织私钥加密而成,而浏览器一般内置了知名的机构的信息与公钥,使用机构提供的公钥解密签名:得到哈希摘要算法,以及一段摘要。接着再对证书上的服务端公钥进行哈希摘要并验证。确保中间过程的服务端公钥没被篡改。

验证完证书的正确性后,客户端会使用一些加密算法(例如:RSA, Diffie-Hellman)产生一个48个字节的Key,这个Key叫PreMaster Secret。该Key将会连同前两个随机数,使用共同协商的加密套件,加密生成“对话密钥(Master Key / Session Key)”。

但客户端只用接收到的公钥加密PreMaster Key,把该加密结果发送给服务端。

此处握手可拓展DH算法生成PreMaster Key,那么就不再需要传递Key,只传递需要的参数即可生成。

第四次 - Server Finish

服务端收到信息后,使用自己的私钥解密,获得PreMaster Key,同样的,因为服务端也有存之前的两个随机数,所以此处也可以根据约定的加密套件生成同样的Master Key

为了验证之前搭建的加密通道是否成功,服务端会用该Master Key加密一段Finish信息给客户端。如果客户端能对Finish信息进行正常加解密且消息正确的被验证,则说明加密通道已经建立成功,可以正常传输数据。

参考资料:
http://www.ruanyifeng.com/blog/2014/09/illustration-ssl.html
https://blog.csdn.net/u010285974/article/details/85320788

浏览器渲染

此处权作通过HTTP请求,浏览器已得到HTML数据:

  1. 对HTML文本词法分析和语法分析,自上而下加载,遇到<script src=""><style href="">此类标签,若资源指向外链,则会额外发起请求加载。

    script标签:若指明defer,则是并行加载,但会延迟到整个页面解析完再执行。async也是会并行加载,加载完后再执行脚本。

  2. 渲染进程将标签内容转换为DOM树

  3. 渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets,计算出DOM节点的样式。

    • 首先进行属性值标准化,例如:将color: blue转化为color: rgb(0,0,255)
    • 其次处理样式的继承与层叠 => 从右往左开始匹配
  4. 创建renderTree布局树,计算元素的布局信息。

    结合dom tree和样式规则,生成一颗只包含可见元素的tree。即display: none的tree并不会出现在此模型上。

  5. 对布局树进行分层,并生成分层树。

    此处考虑到页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-index 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)

  6. 为每个图层生成绘制列表,并将其提交到合成线程。合成线程将图层分图块,并栅格化将图块转换成位图。

    栅格化可以理解为按照图层整合出顶层视角上的显示图块。合成线程优先考虑合成视口及其附近的位图。

  7. 合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上。

参考:https://mp.weixin.qq.com/s/nMlZWZO6foRUPFK34ouPhg

谈谈H5中的audio的播放问题

前言

本文主要针对H5中,使用自定义UI套audio元素,用js控制audio的场景(即不使用自带的control面板)。

播放

<template>
  <div>
    <button @click="playAudio">播放</button>
    <audio src="//xxx.mp3" ref="audio"></audio>
  </div>
</template>

<script>
export default {
  methods: {
    playAudio () {
      this.$refs.audio.play()
    }
  }
}
</script>

上述代码无论在h5还是pc中,都是可行的,因为h5中默认规定在用户交互操作里播放audio。我们在点击事件的回调中进行audioplay,然后请求资源进行播放,符合常理。

但是大多数场景下,audiosrc是动态获取确定的,而播放也不一定是在点击的回调里触发,可能当中经过很多js逻辑,如下面用搭配vuex的例子。

<template>
  <div class="ui-player-audio">
    <audio ref="audio" :src="currentAudio.src"></audio>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
  computed: {
    ...mapGetters(['playing', 'currentAudio'])
  },
  
  watch: {
    playing (status) {
      if (status) {
        this.$refs.audio.play()
      } else {
        this.$refs.audio.pause()
      }
    }
  }
}
</script>

这是一个很典型的播放器业务组件,这时候当我们在别处点了某个音频组件,触发vuexdispatch('PLAY_AUDIO', audioInfo)后,组件监听到状态即会播放。但此处会变成h5浏览器认为是不在用户交互下进行播放,而不会按预期进行。

ps: 这样的设计确实有一定的道理,不然进入某些恶意h5,随便就播放音频,会很讨用户厌恶。

Hack方法:

我们在用户第一次触碰到页面的时候,回调执行加载audioload方法,让浏览器认为是在用户交互操作下触发。

/**
 * 模拟交互操作audio元素
 * @param {Element} audioElement
 * @param {Function} callback?
 */
const interactiveAudio = (audioElment, callback) => {
  const _body = document.documentElement
  const handler = e => {
    e.stopPropagation()
    audioElment && audioElment.load()
    callback && callback()
    _body.removeEventListener('touchstart', handler)
  }
  _body.addEventListener('touchstart', handler)
}

Demo代码可更改为:

<script>
import interactiveAudio from '@/utils/interactiveAudio'
import { mapGetters } from 'vuex'
export default {
  computed: {
    ...mapGetters(['playing', 'currentAudio'])
  },
  
  watch: {
    playing (status) {
      if (status) {
        this.$refs.audio.play()
      } else {
        this.$refs.audio.pause()
      }
    }
  },
  
  mounted () {
    interactiveAudio(this.$refs.audio)
  }
}
</script>

自动播放

设置audioautoplay属性,在h5中是行不通的,理由跟上面一样。在目前为止只看到了微信的Webview浏览器中可以做到,因为其注入了一个钩子事件WeixinJSBridgeReady

 document.addEventListener("WeixinJSBridgeReady", function () {
   const audio = document.getElementById('audio')
   audio.play()
 });

而非微信浏览器的环境,只能通过第一次交互触摸回调,进行播放,这里我们复用上述的interactiveAudio函数

const audio = document.getElmentById('audio')
interactiveAudio(audio, () => audio.play())

单例模式

单例模式的定义:

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

实现单例模式的常用做法:

  1. 使用一个私有属性作标识,存放已经创建的实例。
  2. 构造方法里进行判断:已有实例则返回,无则创建。
  3. 判断时需要考虑多线程异步的情况,保证线程资源的安全。

如果在前端领域里,就不必考虑多线程的情况了。比较经常用到单例的有DialogToast框,例如:用户登录弹框、单次奖品弹框。

针对上述的要点,用JS我们可以用闭包模拟私有属性,奉承单一职责的原则,可以构建一个工厂函数,用于将正常的类加工成单例模式的类。

function singleton (target) {
  let _instance = null

  const _wrapper = function singletoned () {
    if (_instance) {
      return _instance
    } else {
      const ins = Object.create(target.prototype)
      _instance = target.apply(ins, arguments) || ins
      return _instance
    }
  }

  return _wrapper
}

上述代码没有直接用new来创建实例,我们直接用方法模拟new的实现,这样就可以用传递动态参数。

使用方法:

class Target {
  constructor () {
    this.name = 'target' + Date.now()
  }

  say () {
    console.log(this.name)
  }
}

const SingleTarget = singleton(Target)

new SingleTarget().say()
new SingleTarget().say()
new SingleTarget().say()

Webpack打包流程构建原理

前言:此文经过研究《深入浅出Webpack》和 目前webpack源码 来编写

webpack里的几个基本概念:

  • Entry:执行构建的入口,可抽象成输入,并且可以有多个entry。
  • Module:在Webpack里一切皆模块,一个模块对应一个文件。Webpack会从配置的Entry开始,递归找出所有依赖的模块。
  • Loader:模块加载器,用于对模块的原内容按照需求进行加载转换。
  • Plugin:插件,在Webpack构建流程中的特定时,广播对应的事件,此时插件可以监听这些事件的发生,在特定的时机做对应的事情。
  • Chunk:代码块,一个Chunk由多个模块组合而成,用于代码合并与分割。
  • Bundle:打包后产出的一个/多个文件,常见配以[chunk-id] + Hash命名。

整个构建流程大致分为三个部分:

  1. 初始化阶段
  2. 编译阶段
  3. 输出阶段

接下来将从这三个阶段细分,讲讲webpack做了哪些工作:

初始化阶段

  1. 初始化参数

    从配置文件和Shell语句中读取参数并合并,得出最终的配置。

    Shell语句的处理一般由webpack-cli命令行库工具执行,包括--config读取的配置文件,最后才将参数option传递给webpack。这就是为什么使用Webpack时需要安装这两个lib。

    期间如果配置文件(如: webpack.config.js)中Plugins使用了new plugin()之类的语句,则会一并调用,实例化插件对象。

  2. 实例化Compiler

    用得到的参数option初始化Compiler实例,实例中包含了完整的Webpack默认配置。简化代码如下:

    const webpack = (options, callback) => {
      let compiler
      if (Array.isArray(options)) {
       	// ...
        compiler = createMultiCompiler(options);
      } else {
        compiler = createCompiler(options);
      }
      // ...
      return compiler; 
    }

    一般全局只有一个compiler(多份配置option则有多个compiler),并向外暴露run方法进行启动编译。Compiler是负责管理webpack整个打包流程的“ 主人公 ”。

    Compiler主要负责进行:文件监听与编译,初始化编译过程中的事件Hook,到了v4末版本的时候,Hook已多达二十多个,具体点击可查看

    Compiler类中还声明了用于创建子编译对象childCompiler的方法

    /**
      * @param {Compilation} compilation the compilation
      * @param {string} compilerName the compiler's name
      * @param {number} compilerIndex the compiler's index
      * @param {OutputOptions} outputOptions the output options
      * @param {WebpackPluginInstance[]} plugins the plugins to apply
      * @returns {Compiler} a child compiler
    */
    createChildCompiler(
      compilation,
      compilerName,
      compilerIndex,
      outputOptions,
      plugins
    ) {}

    用于Loader/插件有需要时创建,执行模块的分开编译。

  3. Environment

    应用Node的文件系统到compiler对象,方便后续的文件查找和读取

    new NodeEnvironmentPlugin({
      infrastructureLogging: options.infrastructureLogging
    }).apply(compiler);
  4. 加载插件

    依次调用插件的apply方法(默认每个插件对象实例都需要提供一个apply)若为函数则直接调用,将compiler实例作为参数传入,方便插件调用此次构建提供的Webpack API并监听后续的所有事件Hook。

    if (Array.isArray(options.plugins)) {
      for (const plugin of options.plugins) {
        if (typeof plugin === "function") {
          plugin.call(compiler, compiler);
        } else {
          plugin.apply(compiler);
        }
      }
    }
  5. 应用默认的Webpack配置

    applyWebpackOptionsDefaults(options);
    // 随即之后,触发一些Hook
    compiler.hooks.environment.call();
    compiler.hooks.afterEnvironment.call();
    new WebpackOptionsApply().process(options, compiler);
    compiler.hooks.initialize.call();

    除了一些默认的文件系统上下文contextresolver,以及处理的文件输出方式,这里的要应用的默认配置在v4包含新的performance性能优化、Optimization打包优化。

至此,完成了整个第一阶段的初始化。

编译阶段

  1. 启动编译

    这里有个小逻辑区分是否是watch,如果是非watch,则会正常执行一次compiler.run()

    如果是监听文件(如:--watch)的模式,则会传递监听的watchOptions,生成Watching实例,每次变化都重新触发回调。

    function watch(watchOptions, handler) {
      if (this.running) {
        return handler(new ConcurrentCompilationError());
      }
    
      this.running = true;
      this.watchMode = true;
      return new Watching(this, watchOptions, handler);
    }
  2. 触发compile事件

    该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上compiler对象。

  3. Compilation

    这是整个webpack构建打包的关键。每一次的编译(包括watch检测到文件变化时),compiler都会创建一个Compilation对象,标识当前的模块资源、编译生成资源、变化的文件等。同时也提供很多事件回调给插件进行拓展。

    Compilation的生成,是在compiler执行compile方法时构造的,主要流程大概是:触发compile事件后,执行this.newCompilation获取新一轮的compilation,并作为参数触发make事件。然后异步执行此次

    compile (callback) {
      const params = this.newCompilationParams();
      this.hooks.beforeCompile.callAsync(params, err => {
        // ...
      	this.hooks.compile.call(params);
        const compilation = this.newCompilation(params);
        
        // ...
        this.hooks.make.callAsync(compilation, err => {
          //...
          process.nextTick(() => {
    					compilation.finish(err => {
              // ...完成
                this.hooks.afterCompile()
              }
          }                           
      }
    }

    当中还设计到两个主要的钩子:

    • complication:这其实是一个同名的hook,是在上述代码this.newComplication()中调用的,当其调用时已完成complication的实例化。

    • make:表示一个新的Complication创建完毕。

    • after-compile:表示一次Compilation执行完成

    在complication实例化的阶段,调用了Loader转换模块,并将原有的内容结合输出对应的抽象语法树(AST),并递归的分析其导入语句(如import等),最终梳理所有模块的依赖关系形成依赖图谱。

    当所有模块都经过Loader转换完成,此时触发complication的seal事件,根据依赖关系和配置开始着手生成chunk

输出阶段

  1. should-emit事件

    所有需要输出的文件已经生成,询问插件有哪些文件需要输出,哪些不需要输出。

  2. emit事件

    确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出的内容。

  3. after-emit事件

    文件输出完毕

  4. done事件

    成功完成一次完整的编译和输出流程

模拟实现new、bind、call和apply

new

new之后发生了这些事:新对象的原型prototype等于目标的原型prototype,构造函数constructor指向目标,并执行目标函数,隐式返回新对象。

function _new (target, ...args) {
  // 利用该api,以target的prototype为原型生成对象,并包含constructor
  const context = Object.create(target.prototype);
  const ret = target.apply(context, args)
  // 若无显式返回对象 则返回context
  return ret instanceof Object ? ret : context;
}

bind

将函数绑定另一个上下文环境,改变其内部的this指向,同一函数在进行多次bind后,只有第一次bind时能生效,后续无法再改变其上下文。

在日常开发中,有时也有需要不侵入原函数的实现来改变this指向的场景。因此以下模拟一个bind的实现。

function bind (fn, context) {
  return function bindWrapper () {
    return fn.apply(context, arguments);
  }
}

apply

将函数以首个参数作为上下文(改变this),执行后续参数数组,并返回结果。

如果要模拟实现,且不使用bind的话,原理上可以将该函数以一个特殊Key值挂载到指定上下文对象上,然后调用后进行delete即可

Function.prototype._apply = function _apply (context, args) {
  const fn = this;

  // context为null时,应将上下文设为window
  context = context || window;

  // 随机生成一个key值,可自行替换生成的随机规则,这里暂采用时间戳
  const key = +new Date();

  context[key] = fn;

  // 用eval处理参数的传递
  // 保存调用结果,后续返回
  const result = eval('context[key](' + args.map((item, idx) =>{
    return `arguments[1][${idx}]`
  }).join(',') + ')');

  delete context[key];

  return result;
};
call

作用与apply一样,区别就是传递的参数,不以数组的形式,而是以正常传值形式

Function.prototype._call = function _call () {
  const fn = this;
  const context = arguments[0] || window;

  // 截取arguments
  const args = new Array(arguments.length - 1);
  for (let i = 1; i < arguments.length; i++) {
    args[i - 1] = `arguments[${i}]`
  }

  const key = +new Date();

  context[key] = fn;

  const result = eval('context[key](' + args.join(',') + ')');

  delete context[key];

  return result;

}

观察者模式VS发布订阅模式

前言

之前网上看文章,一直以为观察者模式就是相当于发布订阅模式,只是换了个说法,而且很多js开发者跟我有一样的问题。后来接触得多了,才发现其实两者有区别。

首先透过@swnow_in的一张图来谈一下自己的理解

img

从图中可以看出:

  • 观察者与目标(被观察者)是直接进行交互的,包括订阅和触发。
  • 发布订阅模式是透过一个中间者当调度中心,相关的订阅与发布都由调度中心来进行协调。

两者的优缺点:

  • 观察者模式:优点就是一一对应,比较直观明了,占用内存资源容易进行回收。缺点就是两者耦合。
  • 发布订阅模式:优点就是一方面实现了发布者与订阅者之间的解耦,中间者可在两者操作之间进行更细粒度的控制。如:条件过滤发布,权限控制等等。缺点就是整一个中间调度会越来越庞大,需要手动清除里面的发布回调。

举个栗子:

  • 观察者模式:彩票中心里,管理员充当目标对象(被观察者),彩民充当观察者,当管理员说公布一等奖号码时,即给各个观察者发布了消息,然后彩民(观察者)就收到发布消息,进行自己的后续操作(兑奖)。
  • 发布订阅模式:每家每户向牛奶订购中心订购了牛奶,但是各家的牛奶品牌不一样,有燕塘、蒙牛等等。当燕塘牛奶来货了,订阅中心就给订购燕塘的各家各户派发燕塘牛奶。同理,当蒙牛到货时,订阅中心发布蒙牛的牛奶。

观察者模式实现

img

以上面这张图为思路:

首先目标对象(被观察者)称为Subject,有若干个观察者Observer进行观察。当Subject被某些对应事件驱动了,则通知相对应的观察者,调用其回调操作。

代码实现如下,关键部分已上注释:

// 目标(被观察者)对象模型
class Subject{
  constructor () {
    this.obs = [];
  }
  // 添加观察者
  addObserver (ob) {
    this.obs.push(ob);
  }
    
  // 通知所有观察者
  notify () {
    this.obs.forEach( ob => {
      ob.update();
    });
  }
}

// 观察者对象模型
class Observer{
  update(){
    console.log('update');
  }
  // ...
}

// 测试
let subject = new Subject();
let ob = new Observer();
//目标添加观察者了
subject.addObserver(ob);
//目标发布消息调用观察者的更新方法了
subject.notify();   //update

发布订阅模式实现

首先要构造一个总线控制中心,负责中间操作,实现以下三个功能:

  • 订阅xxx消息
  • 发布xxx消息
  • 取消订阅xxx消息

实现代码如下,关键部分已上注释:

// 发布者订阅模式,事件模型
const eventProxy = {
  onList: {},

  // 订阅
  on: function (key, fn) {
    if (!this.onList[key]) {
      this.onList[key] = []
    }
    this.onList[key].push(fn)
  },

  // 取消订阅
  off: function(key, fn) {
    if (!this.onList[key]) return false

    let fnIndex = this.onList[key].indexOf(fn)
    if (fnIndex === -1) return false
    this.onList[key].splice(fnIndex, 1)
    return true
  },
  
  // 发布
  emit: function(...args) {
    if (!args.length) return
	
    // 如果没有任何订阅则返回  
    const key = args[0]
    if (!this.onList[key] || !this.onList[key].length) return
	
    // 发布对应的订阅事件
    const subscriber = this.onList[key]
    const newArgs = args.slice(1)
    subscriber.forEach(cb => {
      cb.apply(null, newArgs)
    })
  }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.