文章概览:node
Appservice层到view层的通讯过程(以setData说明)react
微信小程序采用双线程设计:渲染层的界面使用了WebView进行渲染;逻辑层采用JsCore线程运行JS脚本。
至于这样设计的具体缘由就是管控与安全,能够参看官网双线程设计的介绍。既然视图层与业务逻辑不在同一个线程,那么两者之间的交互就涉及到线程间的通讯过程了。先来看一下官网描述两者通讯过程图:
web
能够看出在真机环境,线程的通讯是经过Native层来负责控制完成,具体的是:小程序
而对于微信开发者工具而言,没有Native,那么它是怎么实现视图层与业务逻辑层之间的通讯呢?一样看一下官网提的图:后端
答案就是两者使用websoket来完成线程间通讯。微信小程序
微信Native是经过分别在视图view层与业务逻辑Appservice层注入WeixinJSBridge来实现两者与Native的通讯,而后Native能够根据状况进行处理或者继续向指定线程传递消息。为了保持与真实环境的一致,微信开发者工具没有新增或者删除WeixinJSBridge的方法,只是重写WeixinJSBridge方法的具体实现。api
webview层加载的view页面,在通过后端处理后会在页面会以script标签的形式注入一些js代码,其中WeixinJSBridge的注入代码的源文件地址为Contents/Resources/app.nw/js/extensions/pageframe/index.js。数组
压缩代码格式后的506~ 560行代码定义了全局的WeixinJSBridge对象,其包括on、invoke、publish和subscribe四个方法来。部分代码以下:安全
window.WeixinJSBridge = { on: o, invoke: a, publish: c, subscribe: u }
能够说微信小程序双线程通讯离不开WeixinJSBridge提供的四个方法,下面介绍下这四个方法的用法及区别:服务器
该方法在view层注册小程序开发者工具触发的事件回调,当小程序开发者工具要触发视图层的某个动做时,借助websocket服务向view层发送command: WEBVIEW_ON_EVENT
命令,标识来自开发者工具触发的消息;而后经过eventName来告诉view层执行什么事件方法
m.a.registerCallback(e => { let { command: t, data: n } = e; "WEBVIEW_ON_EVENT" === t && function (e, t) { let n = h[e]; // h为经过on收集的事件回调 "function" == typeof n && n(t, g.webviewID) }(n.eventName, n.data) });
在微信端则是以api形式调用Native的基础能力。具体过程:
command: WEBVIEW_INVOKE
的命令,根据参数中的api值来肯定调用开发者工具具体的api方法command: WEBVIEW_INVOKE_CALLBACK
命令,view根据此标识知道api调用完毕,而后执行对应的回调function a(e, t, n) { // invoke方法 ... let o = C++; k[o] = n, //k为收集api方法执行后的回调 m.a.send({ // m.a.send方法对websocket的send作了简单封装,为参数添加fromWebviewID参数,其值来自webview的userAgent,下同 command: "WEBVIEW_INVOKE", data: {api: e, args: t, callbackID: o} }) } m.a.registerCallback(e => { let { command: t, data: n } = e; if ("WEBVIEW_INVOKE_CALLBACK" === t) { let e = n.callbackID, t = k[e]; // k为经过invoke收集的api方法执行完后的回调 "function" == typeof t && t(n.res), delete k[e] } });
该过程涉及到双线程的通讯,view层经过websocket服务触发Appservice层的对应事件方法。须要强调的是:
该方法没有收集执行的回调,它只是用来通知Appservice层调用指定的方法,至于执行不执行以及执行结果,view层不关注。
其实现的具体过程以下:
command: WEBVIEW_PUBLISH
的命令,websocket服务接到该命令知道是向Appservice传递消息,就直接向其转发消息eventName
值肯定调用该层注册的事件方法function c(e, t) { // publish方法 m.a.send({ command: "WEBVIEW_PUBLISH", data: { eventName: e, data: t} }) }
view经过该方法注册事件方法,事件方法是Appservice层在某个时间段通知要执行。view层执行回调的标识是收到来自websocket服务的command: APPSERVICE_PUBLISH
命令,经过eventName
来肯定要执行具体什么事件方法。
m.a.registerCallback(e => { let { command: t, data: n, webviewID: o } = e; "APPSERVICE_PUBLISH" === t && function (e, t, n) { let o = N[e]; // N为经过subscribe收集的事件回调 "function" == typeof o && o(t, n) }(n.eventName, n.data) });
Appservice层注入的WeixinJSBridge方法与view层提供的方法相同,可是实现过程区别比较大,可是整体上也是按照command的值来与websocket服务通讯。具体能够参考Contents/Resources/app.nw/js/extensions/appservice/index.js文件。
小程序开发者工具线程间通讯是经过websocket来实现的,经过Contents/Resources/app.nw/js/extensions/pageframe/index.js格式化源码的450~502看出实现结果。下面代码对代码作了修改删减,以便更好的说明实现过程
var socket = null var d = [], s = []; function connect(n) { u = n || u; var l = (window.prompt || window.__global.prompt)('GET_MESSAGE_TOKEN'); var k = window.navigator.userAgent.match(/port\/(\d*)/); var port = k ? parseInt(k[1]) : 9974, var a = new window.WebSocket(`ws://127.0.0.1:${port}`, `${u}#${l}#`)); socket.onopen = function () { let e = [].concat(d); d = [], e.forEach(e => { // socket连接连接后就向其发送消息 send(e) }) }, ... socket.onmessage = function (e) { // 接受websocket服务器传递的消息 ... !function (e) { s.forEach(t => { // 执行registerCallback注册的回调 send.call(this, e) }) }(JSON.parse(e.data)) ... } } function send(e) { socket && socket.readyState === window.WebSocket.OPEN ? socket.send(JSON.stringify(e)) : d.push(e) } function registerCallback(e) { s.push(e) }
上面是开发者工具的实现,在微信环境的实现则是:
IOS经过window.webkit.messageHandlers.invokeHandler.postMessage
来与Native通讯
Android经过X5内核的window.WeixinJSCore.invokeHandler
来与Native通讯
首先强调下,小程序事件对web的事件进行了收敛,只支持如tap、touchstart、touchmove等几种事件,具体支持的事件能够参考小程序官网。除此以外的事件是不被支持的,如click事件等。
就像小程序官网所说,事件是视图层到逻辑层的通信方式。事件能够将用户的行为反馈到逻辑层进行处理。 那么事件究竟是如何在视图层与逻辑层建通讯的呢?下面以view组件的tap事件来作说明,说说小程序事件从view到Appservice层的具体的通讯过程。
小程序采用跟react相似的虚拟dom + 虚拟dom diff的技术来更新dom。经过小程序提供的wcc
可执行程序文件来将小程序wxml模板文件转成虚拟dom,盗用网上的一幅图,虚拟dom大概以下所示:
view层的模板引擎会根据生成的虚拟dom来渲染dom树,在此过程当中,会根据组件的属性来为组件元素绑定指定的事件。这一过程主要是利用:
wxml模板中是采用
bind|catch + 事件名``bind|catch: + 事件名
方式来为指定元素绑定事件;
利用正则能够很容易分析出元素绑定的事件类型及对应的事件回调函数名。注意这一过程都是在view层的js中完成的。微信小程序模板渲染引擎是经过applyProperties(wxElement, attr, raw)
方法来处理元素不一样的属性,其中包括事件绑定;基础版本提供的WAWebview.js文件查看applyProperties
方法的涉及事件部分源码以下
function applyProperties(p, f, A) { // f为元素attr属性对象 ... for (var t in f) e(t) ... var v = p instanceof exparser.Component function e(e) { // 处理attr的每一个属性 var t,n = f[e]; if ("id" === e) {...} if ("slot" === e) {...} if (v && "class" === e ) {...} ... if (t = e.match(/^(capture-)?(bind|catch):?(.+)$/)) { // 使用正则匹配到绑定事件的相关信息 k(g, p, t[3], n, "catch" === t[2], t[1]) ... } ... } // 分析出绑定事件的相关信息,而后为组件元素绑定对应的事件 function k(s, l, c, e, u, t) { // l-为组件元素, c-为绑定的具体事件, e - 为绑定的具体事件回调函数名 var d = t ? "__wxEventCaptureHandleName" : "__wxEventHandleName"; l[d] || (l[d] = Object.create(null)), void 0 === l[d][c] && l.addListener(c, function(e) { // 为组件元素绑定对应的事件 var t = l[d][c]; if (t) { // 该事件对应的回调函数存在触发 ... var a = { type: e.type, timeStamp: e.timeStamp, target: p(e.target, r, this), currentTarget: p(this, r, null), detail: e.detail, touches: A(e.touches), changedTouches: A(e.changedTouches), _requireActive: e._requireActive }; (0, x.sendData)(h.SYNC_EVENT_NAME.WX_EVENT, [s.nodeId.getNodeId(i), t, a]) // sendData方法会通知Appservice层调用指定回调 ... } }, {capture: t }), l[d][c] = null == e ? "" : String(e) // 记录对应的事件回调函数名 }
这样在wxml为元素绑定了事件,在视图层就为小程序元素组件绑定了指定的事件。那么,view层用户对应的行为触发元素绑定的事件,事件内部会调用sendData
方法通知Appservice层调用指定的事件回调函数,具体的参数信息以下:
{ comman: 'WEBVIEW_PUBLISH', data: { eventName: 'vdSync', data: { data: [11, nodeId, eventHandlerName, event], // 数组第一项值为11,表示触发事件;后面依次nodeId,业务层事件回调名称以及事件对象 options: { timestamp: Date.now() } } } }
小程序的tap
事件底层是由web的mouseup
事件转换来的,小程序tap事件的触发分为几个过程:
mouseup
事件window.addEventListener("mouseup", function(e) { !i && a && (t(e, !0), o(e, "touchend"), m(e, e.pageX, e.pageY)) }, { capture: !0, // 捕获事件 passive: !1 });
var i = 3 < arguments.length && void 0 !== arguments[3] && arguments[3] // 建立一个exparser事件,其中t为事件名,tap事件值就是tap,n为mouse事件对象的pageX和pageY组成的对象 , r = exparser.Event.create(t, n, { originalEvent: e, // e为mouseup事件对象 bubbles: !0, capturePhase: !0, composed: !0, extraFields: { _requireActive: i, _allowWriteOnly: !0, touches: e.touches || {}, changedTouches: e.changedTouches || {} } }); exparser.Event.dispatchEvent(e.target, r) //触发目标元素的exparser事件
WEBVIEW_PUBLISH
命令,根据eventName来执行绑定回调事件在view层与Appservice层通讯,统一是发送eventName:vdSync
消息来完成的。首先,Appservice层统一sbuscribe名为vdSync
的回调,而后根据view层消息来找到并执行对应的回调函数。简单看下Appservice层源码:
var s = r() ? ysa._virtualDOMTunnel : __webViewSDK__._virtualDOMTunnel s.onVdSync(function(e, t) { // 先绑定事件 d(e, t) }) function onVdSync(e) { fe("vdSync", e) } function fe(e, s) { // 绑定vdSync回调 ... var n = function(e, t) { var n = e.data , r = e.options; ... "function" == typeof s && s(n, t) // 执行onVdSync绑定的回调 } ... __safeway__.bridge.subscribe(e, n) } // Appservice层接受来自view层的消息 r.registerCallback(t => { let { command: o, data: n, fromWebviewID: r } = t; "WEBVIEW_PUBLISH" === o && e(n.eventName, n.data, r) // 找到并执行对应eventName指定的回调 })
事件从view层触发到通知Appservice层执行对应的事件回调,这一单向流转过程就算完成了;从源码追踪整个事件在双线程间的通讯,实现仍是比较绕的。
与view层到Appservice层单向通讯相似,大概流程是Appservice层来触发消息;view层事先绑定对应消息的处理函数,并根据Appservice层的消息来肯定执行对应处理函数。下面简单以小程序setData
方法来讲下过程。
setData
后会向view层触发消息command: APPSERVICE_PUBLISH
的消息,view层根据该标识知道是来自Appservice层的消息;另外,Appservice层经过统一eventName: vdSyncBatch | vdSync
来指定是Appservice层的setData变动触发的消息,下面以一个例子来讲明。例如页面Page的data字段a属性,经过事件来改变属性a的值:
Page({ data: {a: false}, onTap(){ this.setData({a: !this.data.a} } })
两者交互的消息JSON内容以下:
{ command: 'APPSERVICE_PUBLISH', data: { eventName: 'vdSyncBatch', // setData发送给view层的事件名为vdSyncBatch webviewIds: [1], //对应webview的id数组 data: { data: [ // 比较复杂数据变动状况 [1, 1560416890560], [ "32736897", // nodeId [false, ['a'], true, null] // false为变动前的值,true为变动后的值 ], [0] ], options: { timestamp: Date.now() } } } }
这样就完成了从Appservice层到view层的通讯过程。
从源码追踪的整个过程当中,能够看出小程序在内部实现双线程间的交互过程当中,分别针对不一样的消息指定不一样的标识,简单总结以下:
消息来源标识,使用command字段加以区分
view层传递数据到Appservice层,经过发送command: WEBVIEW_PUBLISH
命令,Appservice层知道消息来自view层,而不是Native层;同理Appservice层经过向view层发送command: APPSERVICE_PUBLISH
命令来加以区分。
同一消息来源下的不一样场景标识区分,使用eventName字段区分
例如上面描述的,view层经过事件场景向Appservice层消息传递是经过eventName: vdSync | vdSyncBatch
形式来加以区分;同理,Appservice层在setData后向view层传递消息也是指定eventName: vdSync | vdSyncBatch
标识