Repository: vuejs/vue
Pull request: [weex] Sync recent changes of Weex

<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
-->

<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->

**What kind of change does this PR introduce?** (check at least one)

- [ ] Bugfix
- [x] Feature
- [ ] Code style update
- [x] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:

**Does this PR introduce a breaking change?** (check one)

- [ ] Yes
- [x] No

**Other information:**

> **These changes will only affect the Weex platform, and will not affect any features on the web.**

For some reasons, the modifications of Weex in the past few month did not merge into this repository in time. This PR contains a series of changes:

+ Support component method.  ([proposal](https://github.com/alibaba/weex/issues/969)) @fkysly 
+ Support for synchronous calls. ([proposal](https://github.com/alibaba/weex/issues/1677)) @fkysly  
+ Refactor CallbackManager. @fkysly 
+ Fix test cases for CallbackManager. @Hanks10100

By the way, the `entry-framework.js` is too long, and the code structure is confusing. It may require a refactoring.

Changed files:
filename: src/platforms/weex/entry-framework.js
patch: @@ -22,7 +22,7 @@ export function init (cfg) {
   renderer.Document = cfg.Document
   renderer.Element = cfg.Element
   renderer.Comment = cfg.Comment
-  renderer.sendTasks = cfg.sendTasks
+  renderer.compileBundle = cfg.compileBundle
 }
 
 /**
@@ -35,7 +35,7 @@ export function reset () {
   delete renderer.Document
   delete renderer.Element
   delete renderer.Comment
-  delete renderer.sendTasks
+  delete renderer.compileBundle
 }
 
 /**
@@ -66,18 +66,9 @@ export function createInstance (
   // Virtual-DOM object.
   const document = new renderer.Document(instanceId, config.bundleUrl)
 
-  // All function/callback of parameters before sent to native
-  // will be converted as an id. So `callbacks` is used to store
-  // these real functions. When a callback invoked and won't be
-  // called again, it should be removed from here automatically.
-  const callbacks = []
-
-  // The latest callback id, incremental.
-  const callbackId = 1
-
   const instance = instances[instanceId] = {
     instanceId, config, data,
-    document, callbacks, callbackId
+    document
   }
 
   // Prepare native module getter and HTML5 Timer APIs.
@@ -102,11 +93,16 @@ export function createInstance (
     weex: weexInstanceVar,
     // deprecated
     __weex_require_module__: weexInstanceVar.requireModule // eslint-disable-line
-  }, timerAPIs)
-  callFunction(instanceVars, appCode)
+  }, timerAPIs, env.services)
+
+  if (!callFunctionNative(instanceVars, appCode)) {
+    // If failed to compile functionBody on native side,
+    // fallback to 'callFunction()'.
+    callFunction(instanceVars, appCode)
+  }
 
   // Send `createFinish` signal to native.
-  renderer.sendTasks(instanceId + '', [{ module: 'dom', method: 'createFinish', args: [] }], -1)
+  instance.document.taskCenter.send('dom', { action: 'createFinish' }, [])
 }
 
 /**
@@ -117,6 +113,7 @@ export function createInstance (
 export function destroyInstance (instanceId) {
   const instance = instances[instanceId]
   if (instance && instance.app instanceof instance.Vue) {
+    instance.document.destroy()
     instance.app.$destroy()
   }
   delete instances[instanceId]
@@ -138,7 +135,7 @@ export function refreshInstance (instanceId, data) {
     instance.Vue.set(instance.app, key, data[key])
   }
   // Finally `refreshFinish` signal needed.
-  renderer.sendTasks(instanceId + '', [{ module: 'dom', method: 'refreshFinish', args: [] }], -1)
+  instance.document.taskCenter.send('dom', { action: 'refreshFinish' }, [])
 }
 
 /**
@@ -153,41 +150,51 @@ export function getRoot (instanceId) {
   return instance.app.$el.toJSON()
 }
 
+const jsHandlers = {
+  fireEvent: (id, ...args) => {
+    return fireEvent(instances[id], ...args)
+  },
+  callback: (id, ...args) => {
+    return callback(instances[id], ...args)
+  }
+}
+
+function fireEvent (instance, nodeId, type, e, domChanges) {
+  const el = instance.document.getRef(nodeId)
+  if (el) {
+    return instance.document.fireEvent(el, type, e, domChanges)
+  }
+  return new Error(`invalid element reference "${nodeId}"`)
+}
+
+function callback (instance, callbackId, data, ifKeepAlive) {
+  const result = instance.document.taskCenter.callback(callbackId, data, ifKeepAlive)
+  instance.document.taskCenter.send('dom', { action: 'updateFinish' }, [])
+  return result
+}
+
 /**
- * Receive tasks from native. Generally there are two types of tasks:
- * 1. `fireEvent`: an device actions or user actions from native.
- * 2. `callback`: invoke function which sent to native as a parameter before.
- * @param {string} instanceId
- * @param {array}  tasks
+ * Accept calls from native (event or callback).
+ *
+ * @param  {string} id
+ * @param  {array} tasks list with `method` and `args`
  */
-export function receiveTasks (instanceId, tasks) {
-  const instance = instances[instanceId]
-  if (!instance || !(instance.app instanceof instance.Vue)) {
-    return new Error(`receiveTasks: instance ${instanceId} not found!`)
-  }
-  const { callbacks, document } = instance
-  tasks.forEach(task => {
-    // `fireEvent` case: find the event target and fire.
-    if (task.method === 'fireEvent') {
-      const [nodeId, type, e, domChanges] = task.args
-      const el = document.getRef(nodeId)
-      document.fireEvent(el, type, e, domChanges)
-    }
-    // `callback` case: find the callback by id and call it.
-    if (task.method === 'callback') {
-      const [callbackId, data, ifKeepAlive] = task.args
-      const callback = callbacks[callbackId]
-      if (typeof callback === 'function') {
-        callback(data)
-        // Remove the callback from `callbacks` if it won't called again.
-        if (typeof ifKeepAlive === 'undefined' || ifKeepAlive === false) {
-          callbacks[callbackId] = undefined
-        }
+export function receiveTasks (id, tasks) {
+  const instance = instances[id]
+  if (instance && Array.isArray(tasks)) {
+    const results = []
+    tasks.forEach((task) => {
+      const handler = jsHandlers[task.method]
+      const args = [...task.args]
+      /* istanbul ignore else */
+      if (typeof handler === 'function') {
+        args.unshift(id)
+        results.push(handler(...args))
       }
-    }
-  })
-  // Finally `updateFinish` signal needed.
-  renderer.sendTasks(instanceId + '', [{ module: 'dom', method: 'updateFinish', args: [] }], -1)
+    })
+    return results
+  }
+  return new Error(`invalid instance id "${id}" or tasks`)
 }
 
 /**
@@ -288,9 +295,7 @@ function createVueModuleInstance (instanceId, moduleGetter) {
  * Generate native module getter. Each native module has several
  * methods to call. And all the behaviors is instance-related. So
  * this getter will return a set of methods which additionally
- * send current instance id to native when called. Also the args
- * will be normalized into "safe" value. For example function arg
- * will be converted into a callback id.
+ * send current instance id to native when called.
  * @param  {string}  instanceId
  * @return {function}
  */
@@ -300,12 +305,20 @@ function genModuleGetter (instanceId) {
     const nativeModule = modules[name] || []
     const output = {}
     for (const methodName in nativeModule) {
-      output[methodName] = (...args) => {
-        const finalArgs = args.map(value => {
-          return normalize(value, instance)
-        })
-        renderer.sendTasks(instanceId + '', [{ module: name, method: methodName, args: finalArgs }], -1)
-      }
+      Object.defineProperty(output, methodName, {
+        enumerable: true,
+        configurable: true,
+        get: function proxyGetter () {
+          return (...args) => {
+            return instance.document.taskCenter.send('module', { module: name, method: methodName }, args)
+          }
+        },
+        set: function proxySetter (val) {
+          if (typeof val === 'function') {
+            return instance.document.taskCenter.send('module', { module: name, method: methodName }, [val])
+          }
+        }
+      })
     }
     return output
   }
@@ -328,15 +341,17 @@ function getInstanceTimer (instanceId, moduleGetter) {
       const handler = function () {
         args[0](...args.slice(2))
       }
+
       timer.setTimeout(handler, args[1])
-      return instance.callbackId.toString()
+      return instance.document.taskCenter.callbackManager.lastCallbackId.toString()
     },
     setInterval: (...args) => {
       const handler = function () {
         args[0](...args.slice(2))
       }
+
       timer.setInterval(handler, args[1])
-      return instance.callbackId.toString()
+      return instance.document.taskCenter.callbackManager.lastCallbackId.toString()
     },
     clearTimeout: (n) => {
       timer.clearTimeout(n)
@@ -368,50 +383,53 @@ function callFunction (globalObjects, body) {
 }
 
 /**
- * Convert all type of values into "safe" format to send to native.
- * 1. A `function` will be converted into callback id.
- * 2. An `Element` object will be converted into `ref`.
- * The `instance` param is used to generate callback id and store
- * function if necessary.
- * @param  {any}    v
- * @param  {object} instance
- * @return {any}
+ * Call a new function generated on the V8 native side.
+ *
+ * This function helps speed up bundle compiling. Normally, the V8
+ * engine needs to download, parse, and compile a bundle on every
+ * visit. If 'compileBundle()' is available on native side,
+ * the downloding, parsing, and compiling steps would be skipped.
+ * @param  {object} globalObjects
+ * @param  {string} body
+ * @return {boolean}
  */
-function normalize (v, instance) {
-  const type = typof(v)
-
-  switch (type) {
-    case 'undefined':
-    case 'null':
-      return ''
-    case 'regexp':
-      return v.toString()
-    case 'date':
-      return v.toISOString()
-    case 'number':
-    case 'string':
-    case 'boolean':
-    case 'array':
-    case 'object':
-      if (v instanceof renderer.Element) {
-        return v.ref
-      }
-      return v
-    case 'function':
-      instance.callbacks[++instance.callbackId] = v
-      return instance.callbackId.toString()
-    default:
-      return JSON.stringify(v)
+function callFunctionNative (globalObjects, body) {
+  if (typeof renderer.compileBundle !== 'function') {
+    return false
   }
-}
 
-/**
- * Get the exact type of an object by `toString()`. For example call
- * `toString()` on an array will be returned `[object Array]`.
- * @param  {any}    v
- * @return {string}
- */
-function typof (v) {
-  const s = Object.prototype.toString.call(v)
-  return s.substring(8, s.length - 1).toLowerCase()
+  let fn = void 0
+  let isNativeCompileOk = false
+  let script = '(function ('
+  const globalKeys = []
+  const globalValues = []
+  for (const key in globalObjects) {
+    globalKeys.push(key)
+    globalValues.push(globalObjects[key])
+  }
+  for (let i = 0; i < globalKeys.length - 1; ++i) {
+    script += globalKeys[i]
+    script += ','
+  }
+  script += globalKeys[globalKeys.length - 1]
+  script += ') {'
+  script += body
+  script += '} )'
+
+  try {
+    const weex = globalObjects.weex || {}
+    const config = weex.config || {}
+    fn = renderer.compileBundle(script,
+      config.bundleUrl,
+      config.bundleDigest,
+      config.codeCachePath)
+    if (fn && typeof fn === 'function') {
+      fn(...globalValues)
+      isNativeCompileOk = true
+    }
+  } catch (e) {
+    console.error(e)
+  }
+
+  return isNativeCompileOk
 }

filename: test/weex/runtime/framework.spec.js
patch: @@ -248,8 +248,8 @@ describe('framework APIs', () => {
         { method: 'fireEvent', args: [textRef, 'click'] }
       ])
       expect(result instanceof Error).toBe(true)
-      expect(result).toMatch(/receiveTasks/)
-      expect(result).toMatch(/not found/)
+      expect(result).toMatch(/invalid\sinstance\sid/)
+      expect(result).toMatch(instance.id)
       done()
     })
   })
@@ -341,8 +341,8 @@ describe('framework APIs', () => {
             { method: 'callback', args: [callbackId] }
           ])
           expect(result instanceof Error).toBe(true)
-          expect(result).toMatch(/receiveTasks/)
-          expect(result).toMatch(/not found/)
+          expect(result).toMatch(/invalid\sinstance\sid/)
+          expect(result).toMatch(instance.id)
           done()
         })
       })