pendo, ga脚本的部分原理和this
这篇文章有2个话题——数据收集技术和this. 数据收集技术主要是解析pendo/ga脚本的部分原理;this,就是javascript中的this, 我感觉每年都会从javascript weekly等newsletter中看到几篇文章讲解this, 而且它们都称作是 the last article about javascript this.
Hi,
这篇文章有2个话题——数据收集技术和this. 数据收集技术主要是解析pendo/ga脚本的部分原理;this,就是javascript中的this, 我感觉每年都会从javascript weekly等newsletter中看到几篇文章讲解this, 而且它们都称作是 the last article about javascript this.
pendo/ga脚本的部分原理
先看ga吧,这个比较简单
GA
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');
//---------------------------------------------------
function installGA() {
window['GoogleAnalyticsObject'] = 'ga'; //----- line 1
window['ga'] = window['ga'] || function () { //
window['ga'].q = window['ga'].q || []; //
window['ga'].q.push(arguments); //
}; //
window['ga'].l = 1 * new Date(); //----- line 6
a = document.createElement('script'); //----- line 7
a.async = 1;
a.src = 'https://www.google-analytics.com/analytics.js'
m = document.getElementsByTagName('script')[0];
m.parentNode.insertBefore(a, m); //----- line 11
ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');
};
上面一部分是ga官方提供的脚本,下面是我改写的,为了方便理解。说ga简单是因为这段脚本只做了2件事:
- 构造一个功能非常基础的ga对象 (line 1-6)
- 加载完整版的ga功能 (line 7-11)
非常基础的ga功能仅仅能做一件事:把参数存起来。例如在初始化ga的时候,ga('create', 'UA-xxx', 'auto');
仅仅是把3个参数('create', 'UA-xxx', 'auto')放在一个叫q
的数组中,当然放进去的是一个Argument
对象,类似一个array, 所以在调用结束后,q
中大约是这样:
[
Argument['create', 'UA-xxx', 'auto'],
]
类似的,第二次调用ga也会把参数放在q
中,那么接下来呢?接下来当ga完整版功能加载到浏览器中后,会处理q
中的数据,所以这里的q
就是一个queue, 处理完后会被清除。
Pendo
下面可以看一下复杂一点的pendo脚本
(function (apiKey) {
(function(p,e,n,d,o){var v,w,x,y,z;o=p[d]=p[d]||{};o._q=[];
v=['initialize','identify','updateOptions','pageLoad'];for(w=0,x=v.length;w<x;++w)(function(m){
o[m]=o[m]||function(){o._q[m===v[0]?'unshift':'push']([m].concat([].slice.call(arguments,0)));};})(v[w]);
y=e.createElement(n);y.async=!0;y.src='https://cdn.pendo.io/agent/static/'+apiKey+'/pendo.js';
z=e.getElementsByTagName(n)[0];z.parentNode.insertBefore(y,z);})(window,document,'script','pendo');
})('<ACTUAL_API_KEY_HERE>');
// o._q[m===v[0]?'unshift':'push']是什么意思?
// 其实就是在选择函数
// []["length"]的作用和[].length一样
// 这里的意思是如果你调用initialize方法,就用[].unshift,否则用[].push
//--------------------------------------------------------
function installPendo() {
window['pendo'] = window['pendo'] || {};
o = window['pendo'];
o._q = [];
// 情怀如下
['initialize','identify','updateOptions','pageLoad'].forEach(methodName => {
window['pendo'][methodName] = window['pendo'][methodName] || function () {
if (methodName === 'initialize') {
o._q.unshift(['initialize'].concat([].slice.call(arguments, 0)));
} else {
o._q.push([methodName].concat([].slice.call(arguments, 0)));
}
};
})
y = document.createElement('script');
y.async = 1;
y.src = 'https://cdn.pendo.io/agent/static/YOURAPIKEY/pendo.js';
z = document.getElementsByTagName('script');
z.parentNode.insertBefore(y, z);
pendo.initialize({
visitor: {
id: 1,
other_prop: 2,
another_prop: 3
},
account: {
}
})
}
这个脚本比较有情怀,从参数的名字上可以看出:function(p,e,n,d,o)
,其实o
是凑数的。
因为有GA脚本的铺垫,pendo脚本的原理很容易看出,也是做了2件事:
- 构造基础版pendo
- 加载完整版
第二点情怀体现在构造了一个看起来比较真实的基础版pendo:添加了几个方法,initialize, identify, updateOptions, pageLoad, ...这几个方法除了initialize比较特殊外,其他的方法和GA类似,把参数先收着。
pendo在处理参数的时候也比较情怀(nan dong),大约用了这样的方法处理arguments:o._q.push([methodName].concat([].slice.call(arguments, 0)))
,在第二部分讲this的时候会提到它。
当调用pendo.initialize或者pendo.pageLoad的时候,queue的样子大约是:
[
['initialize', {visitor:{id:1, xxx: 2}, account:{id: 3}}],
['pageLoad', {...}]
]
initialize特殊的地方在于,不管什么时候调用,都会被放在queue最前面,这是由于initialize用了unshift方法而不是push,具体可以参考js array unshift方法。
等pendo完整版加载后,queue会被处理。
最后猜测一下GA为啥不做一个情怀版的,可能是少一个字符能省下很多流量费,能精简就精简吧。
第一部分结束,通过阅读pendo/ga脚本学到了这种设计技巧:
- 可以通过精简版的功能接着请求,等完整版加载完后再处理
- 容易扩展功能,比如pendu希望提供
trackEvent
功能只需要在method list中再增加一个就行,GA更简单,只需要调用的时候第一个参数写成trackEvent
js中的this
本来没有这部分的计划,是因为pendo脚本中的这段代码引出
[].slice.call(arguments, 0)
这到底在干嘛?把argument处理成array,前面说过argument是一个array-like的对象。array-like意思是拥有length
属性,并且可以从0开始访问数据,但没有array该有的那些方法。
Arguments不是很常用,简单来说,它是在function中可以访问到的一个特殊对象,代表了这个function的参数,例如
function f(a, b, c) {console.log(a, b, c, arguments);}
f(1,2,3)
// 1 2 3 Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
GA选择了保存整个argument对象,而pendo选择了把它变成array存起来,变成array的办法就是上面的那段代码。
slice是array的一个方法,可以把array分割,返回一个新array。
为什么要用call而不是直接arguments.slice()?因为arguments是array-like, 没有slice方法。。。
那call做了什么?call把slice的this改成了arguments,并且把后面的0作为参数传给了slice,用slice处理arguments应该算是一种技巧,能使用这种技巧是有些前提条件的——slice正好只依赖this指向的那个对象有length
。更好的做法自然是让arguments对象支持toArray之类的操作,当然,ES2015带来了Array.from
和spread syntax
,同样可以很简单地转成array,但浏览器兼容性显然没有slice那么好。
终于出现了this
当我们在global中,或者说浏览器的窗口中定义一个这样的函数并执行,你觉得this是什么?
function f() {console.log(this);}
f()
是Window,不信你可以自己试。
那如果用nodejs运行上面的代码,你觉得this是什么?
是一个叫Global的东西,大约这样,非常像window:
<ref *1> Object [global] {
global: [Circular *1],
clearInterval: [Function: clearInterval],
clearTimeout: [Function: clearTimeout],
setInterval: [Function: setInterval],
setTimeout: [Function: setTimeout] {
[Symbol(nodejs.util.promisify.custom)]: [Getter]
},
queueMicrotask: [Function: queueMicrotask],
clearImmediate: [Function: clearImmediate],
setImmediate: [Function: setImmediate] {
[Symbol(nodejs.util.promisify.custom)]: [Getter]
}
}
这里我想说的是,当你在全局空间中执行函数f的时候,实际上是这样执行的:
- 在浏览器中:
window.f()
- 在nodejs中:
global.f()
函数f中的this会被设置成window或者global,那么,当运行abc.func()
的时候,func中的this是什么?当然是abc
了
下面有两个特别的情况:
- new操作
- call/apply
function Product(category){this.category = category;}
Product('tech') -> window.category is 'tech'
p = new Product('tech') -> p.category is 'tech'
如果把Product当作一个函数执行,那么在浏览器中,就会在window对象上添加category属性。
如果用new来操作,会得到一个对象,并且对象会有一个category属性。从这里可以看到,new操作同样做了修改this指向的事情:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new
call/apply类似,会修改被调用函数的this指向。
再次看一下这一行代码
[].slice.call(arguments, 0)
调用array上的slice方法时,例如
[1,2,3,4].slice(2, 3)
// -> [3]
首先要明确一点,slice方法中如果用了this,那么this指的是什么?当然是[1,2,3,4]
所以用call的作用就是把slice中的this指向修改掉,修改成arguments, 就相当于在执行arguments.slice(0)
一样,如果没想明白可以慢慢体会一下?
剩下的this
arrow function
const outerThis = this;
const arrowFunc = () => {
console.log(this === outerThis);
}
arrowFunc();
// -> true
箭头函数,函数里面的this,总是和函数外的this一致,这和普通函数不一样。
另外一个不一样,arrow function不能当作constuctor, 也就是不能new
new arrowFunc()
// -> Uncaught TypeError: arrowFunc is not a constructor
可以修改this的函数
part2中提到了call和apply,其实还有bind.
bind返回一个新函数,而且把函数中的this改成了你传给它的那个,例如:
// Global is Window
function func() {console.log(this);}
func() // -> Window {window: Window, self: Window, document: document,...}
func.bind({name: 'new this'})(); // -> {name: 'new this'} instead of Window
然而这些可以修改this的函数,对于arrow function来说,不起作用
还有很多和this相关的东西
可以参考这里:https://web.dev/javascript-this/, 以及那些last articals about js this