记录各种原理体会,代码实践=
impeiran / blog Goto Github PK
View Code? Open in Web Editor NEWBlogs about Technology - 一些技术文章
Blogs about Technology - 一些技术文章
记录各种原理体会,代码实践=
单例模式的定义:
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
实现单例模式的常用做法:
如果在前端领域里,就不必考虑多线程的情况了。比较经常用到单例的有Dialog
、Toast
框,例如:用户登录弹框、单次奖品弹框。
针对上述的要点,用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()
本文将把vue 2.x的响应式原理及其依赖收集进行分析讲解,并简单用代码模拟一遍vue这一部分的源码。
(此处及之后的vue泛指2.x
版本的vue)
绝大部分人都知道vue2.x的响应式依赖一个APIObject.defineProperty
,通过该API可以劫持属性的获取(get)/赋值(set),在回调中进行更新视图,从而达到由数据驱动视图去更新。
但要实现响应式,细节上还需要更多,譬如:
属性被set之后,具备什么样的条件才能更新视图。
上述第一点满足后,框架如何知道具体要更新哪一个视图组件。
如何解决多个属性触发同一个组件更新的情况。
...
抛出上述问题之后,vue的做法是这样的:
data()
之后,每个属性key
作为一个依赖,实例化一个名为Dep的依赖对象const dep = new Dep()
,并用Object.defineProperty
劫持get
/set
,
getter
,在getter
的回调中收集其对应的依赖dep
set
属性时,在属性的setter
回调中,其对应dep
通知所有收集到它的对象。watcher
对应到每一个组件,Vue把其称为render watcher
。属性被set
之后,依赖对象dep
就会通知将它收集的watcher
,由watcher
进行更新视图。引用vue官方的一张图展示这一个过程:
首先组件render
的时候,渲染在视图中的状态都会触发其getter
,然后组件对应的Watcher
在getter
回调中将其作为依赖进行收集。当状态发生变化后,通知notify
收集其依赖的Watcher
,然后Watcher
进行更新,触发组件的重新render。
接下来由代码来讲述这整个流程,基本就是vue源代码的简化版,省略了大部分的变量校验和与本文主题无关的代码。想仔细研究完整版本请自行查阅源码。
首先我们要拿到组件中声明的状态,默认规定是一个工厂函数,返回一个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()
}
})
}
}
这是递归遍历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
有两个设计点:
render watcher
收集,也可以被user watcher
收集,即用户自己编写的watch
选项,等等。target
,存放当前进行render
的watcher
。虽然说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
的设计了。
vue里面的Watcher
,负责了三个功能:computed
、用户自定义watcher
、data
状态的依赖收集,而前两者都有依赖于第三个依赖收集的机制。源码里的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
上一次收集过的依赖,赋值给新的依赖。就可以避免改变视图中没用到的状态,也会触发更新这个场景,可以说是一种优化。
结合了上述编写的简单Dep
和Watcher
,简单写一个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是File://
协议开头,浏览器首先会查找机子上的本地文件。如果是FTP
就会按照其对应规则建立连接。如果是http/https
则会按照下面步骤进行。
如果地址是直接使用ip时,则跳过域名解析阶段。
地址是使用域名型的,就会进行此步骤,涉及到的概念:
整个查找解析是一个递归过程:首先从
1 客户端/浏览器缓存中查找 ->
2 本地的hosts文件查找 ->
3 本地DNS解析器(此时可能命中缓存) ->
4 本地DNS服务器 ->
当上述这个过程都未命中时,根据本地DNS服务器设置的转发器进行查询。若未用转发模式,则迭代查找过程如下:
5 根域名服务器 ->
6 顶级域名服务器 ->
7 权威域名服务器
缓存与优化
如果是http(s)的链接,还是基于tcp的,需要建立起链接。涉及到TCP的三次握手,主要作用是为了确认双方的接收能力和发送能力是否都正确,指定自己的初始化序列号为后面的可靠性传送做准备。
涉及概念:
一开始:客户端处于Closed
状态,服务端处于Listen
状态。
发起方A向B发起连接请求报文段,tcp首部的控制位SYN设为1,序号seq=x(某个开始的传输字节数)。此时SYN=1时不携带数据,但要消耗一个序号位。此时发起方进入SYN-SENT(同步已发送)阶段。
接收方B收到请求连接报文,若同意建立连接,则发送报文段:设控制位ACK=1,SYN继续为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继续确认进行第二次握手,重复建立链接。
有了第三次握手,则会确保第一次延误时不会进行多次重复的连接。
TLS以SSL3.0为基准,后又制定了TLS1.0、TLS1.1和TLS1.2。当前主流的版本是SSL3.0和TLS1.0。
整个握手过程采用非对称加密与对称加密混合的方式。因为全部使用非对称加密的方式,算法实现会很耗时间,整个传输过程就会变得效率低。全部采用对称加密的方式,一开始若密钥被监听到,则就不安全。
所以正常数据传输时采用对称加密,而对称加密的密钥,采用安全性更好的非对称加密进行传输。
客户端首先要告诉服务端,自己可以支持哪些加密算法。于是客户端把本地支持的加密套件(cipher suites),以及产生一个随机数(Client Random),两者一并传输给服务端。而且,客户端也要保存这个随机数。
服务端收到信息后,保存第一次的随机数。然后把证书(包含公钥、数字签名、过期信息、其余网站信息),以及生成并保存第二个随机数(Server Random),并确定使用的加密方法,三个信息一并传输回给客户端(server hello done)。
若此次发送的证书信息不齐全时,中间还会发送一个
Server Key Exchange
信息,补充回给客户端。
接着,客户端收到证书及信息后。首先验证证书的完整性、正确性、以及证书上的域名与服务端域名是否一致。
拓展数字签名:证书的数字签名由CA相关组织私钥加密而成,而浏览器一般内置了知名的机构的信息与公钥,使用机构提供的公钥解密签名:得到哈希摘要算法,以及一段摘要。接着再对证书上的服务端公钥进行哈希摘要并验证。确保中间过程的服务端公钥没被篡改。
验证完证书的正确性后,客户端会使用一些加密算法(例如:RSA, Diffie-Hellman)产生一个48个字节的Key,这个Key叫PreMaster Secret。该Key将会连同前两个随机数,使用共同协商的加密套件,加密生成“对话密钥(Master Key / Session Key)”。
但客户端只用接收到的公钥加密PreMaster Key,把该加密结果发送给服务端。
此处握手可拓展DH算法生成PreMaster Key,那么就不再需要传递Key,只传递需要的参数即可生成。
服务端收到信息后,使用自己的私钥解密,获得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数据:
对HTML文本词法分析和语法分析,自上而下加载,遇到<script src="">
和<style href="">
此类标签,若资源指向外链,则会额外发起请求加载。
script标签:若指明defer,则是并行加载,但会延迟到整个页面解析完再执行。async也是会并行加载,加载完后再执行脚本。
渲染进程将标签内容转换为DOM树
渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets
,计算出DOM节点的样式。
- 首先进行属性值标准化,例如:将
color: blue
转化为color: rgb(0,0,255)
- 其次处理样式的继承与层叠 => 从右往左开始匹配
创建renderTree
布局树,计算元素的布局信息。
结合dom tree和样式规则,生成一颗只包含可见元素的tree。即display: none的tree并不会出现在此模型上。
对布局树进行分层,并生成分层树。
此处考虑到页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-index 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)
为每个图层生成绘制列表,并将其提交到合成线程。合成线程将图层分图块,并栅格化将图块转换成位图。
栅格化可以理解为按照图层整合出顶层视角上的显示图块。合成线程优先考虑合成视口及其附近的位图。
合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上。
前言:此文经过研究《深入浅出Webpack》和 目前webpack源码 来编写
webpack里的几个基本概念:
整个构建流程大致分为三个部分:
接下来将从这三个阶段细分,讲讲webpack做了哪些工作:
初始化参数
从配置文件和Shell语句中读取参数并合并,得出最终的配置。
Shell语句的处理一般由webpack-cli
命令行库工具执行,包括--config
读取的配置文件,最后才将参数option
传递给webpack
。这就是为什么使用Webpack时需要安装这两个lib。
期间如果配置文件(如: webpack.config.js
)中Plugins使用了new plugin()
之类的语句,则会一并调用,实例化插件对象。
实例化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/插件有需要时创建,执行模块的分开编译。
Environment
应用Node的文件系统到compiler对象,方便后续的文件查找和读取
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
加载插件
依次调用插件的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);
}
}
}
应用默认的Webpack配置
applyWebpackOptionsDefaults(options);
// 随即之后,触发一些Hook
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
除了一些默认的文件系统上下文context
、resolver
,以及处理的文件输出方式,这里的要应用的默认配置在v4包含新的performance
性能优化、Optimization
打包优化。
至此,完成了整个第一阶段的初始化。
启动编译
这里有个小逻辑区分是否是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);
}
触发compile事件
该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上compiler对象。
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
。
should-emit事件
所有需要输出的文件已经生成,询问插件有哪些文件需要输出,哪些不需要输出。
emit事件
确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出的内容。
after-emit事件
文件输出完毕
done事件
成功完成一次完整的编译和输出流程
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
}
前言
之前网上看文章,一直以为观察者模式就是相当于发布订阅模式,只是换了个说法,而且很多js开发者跟我有一样的问题。后来接触得多了,才发现其实两者有区别。
首先透过@swnow_in的一张图来谈一下自己的理解
从图中可以看出:
两者的优缺点:
举个栗子:
以上面这张图为思路:
首先目标对象(被观察者)称为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
首先要构造一个总线控制中心,负责中间操作,实现以下三个功能:
实现代码如下,关键部分已上注释:
// 发布者订阅模式,事件模型
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)
})
}
}
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;
}
将函数绑定另一个上下文环境,改变其内部的this指向,同一函数在进行多次bind后,只有第一次bind时能生效,后续无法再改变其上下文。
在日常开发中,有时也有需要不侵入原函数的实现来改变this指向的场景。因此以下模拟一个bind的实现。
function bind (fn, context) {
return function bindWrapper () {
return fn.apply(context, arguments);
}
}
将函数以首个参数作为上下文(改变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;
};
作用与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;
}
本文主要针对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
。我们在点击事件的回调中进行audio
的play
,然后请求资源进行播放,符合常理。
但是大多数场景下,audio
的src
是动态获取确定的,而播放也不一定是在点击的回调里触发,可能当中经过很多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>
这是一个很典型的播放器业务组件,这时候当我们在别处点了某个音频组件,触发vuex
的dispatch('PLAY_AUDIO', audioInfo)
后,组件监听到状态即会播放。但此处会变成h5浏览器认为是不在用户交互下进行播放,而不会按预期进行。
ps: 这样的设计确实有一定的道理,不然进入某些恶意h5,随便就播放音频,会很讨用户厌恶。
Hack方法:
我们在用户第一次触碰到页面的时候,回调执行加载audio
的load
方法,让浏览器认为是在用户交互操作下触发。
/**
* 模拟交互操作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>
设置audio
的autoplay
属性,在h5中是行不通的,理由跟上面一样。在目前为止只看到了微信的Webview浏览器中可以做到,因为其注入了一个钩子事件WeixinJSBridgeReady
。
document.addEventListener("WeixinJSBridgeReady", function () {
const audio = document.getElementById('audio')
audio.play()
});
而非微信浏览器的环境,只能通过第一次交互触摸回调,进行播放,这里我们复用上述的interactiveAudio
函数
const audio = document.getElmentById('audio')
interactiveAudio(audio, () => audio.play())
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.