Преглед изворни кода

feat(weex): partially support lifecycles of virtual component (#7242)

Update the `_init` and `_update` logic to partially support lifecycles.
Add test cases for testing the lifecycle hooks and data update.
Hanks пре 8 година
родитељ
комит
661bfe552e

+ 1 - 0
flow/component.js

@@ -70,6 +70,7 @@ declare interface Component {
   _hasHookEvent: boolean;
   _provided: ?Object;
   _inlineComputed: ?{ [key: string]: Watcher }; // inline computed watchers for literal props
+  // _virtualComponents?: { [key: string]: Component };
 
   // private methods
 

+ 2 - 0
flow/options.js

@@ -11,6 +11,8 @@ declare type InternalComponentOptions = {
 type InjectKey = string | Symbol;
 
 declare type ComponentOptions = {
+  componentId?: string;
+
   // data
   data: Object | Function | void;
   props?: { [key: string]: PropOptions };

+ 3 - 3
package-lock.json

@@ -9796,9 +9796,9 @@
       }
     },
     "weex-js-runtime": {
-      "version": "0.23.4",
-      "resolved": "https://registry.npmjs.org/weex-js-runtime/-/weex-js-runtime-0.23.4.tgz",
-      "integrity": "sha512-leBzBrpvbrKHvmwd00YjzxZAQrp6NWEbEt3jAtk5EINccxZzFhwZ8SpUmn0d5bspemFisT18eJDAavv4LgRxpw==",
+      "version": "0.23.5",
+      "resolved": "https://registry.npmjs.org/weex-js-runtime/-/weex-js-runtime-0.23.5.tgz",
+      "integrity": "sha512-94/bMUpCyZMsrq2codDPFatr5Ec8yKYKYNsfoshQOiQKTZY3pwqlfedtOKQNf6k7o4npEhdxDnHJwEVORtNylg==",
       "dev": true
     },
     "weex-styler": {

+ 1 - 1
package.json

@@ -125,7 +125,7 @@
     "typescript": "^2.6.1",
     "uglify-js": "^3.0.15",
     "webpack": "^3.10.0",
-    "weex-js-runtime": "^0.23.3",
+    "weex-js-runtime": "^0.23.5",
     "weex-styler": "^0.3.0"
   },
   "config": {

+ 1 - 1
src/core/instance/state.js

@@ -150,7 +150,7 @@ function initData (vm: Component) {
   observe(data, true /* asRootData */)
 }
 
-function getData (data: Function, vm: Component): any {
+export function getData (data: Function, vm: Component): any {
   try {
     return data.call(vm, vm)
   } catch (e) {

+ 57 - 11
src/platforms/weex/runtime/recycle-list/virtual-component.js

@@ -2,13 +2,14 @@
 
 // https://github.com/Hanks10100/weex-native-directive/tree/master/component
 
-import { mergeOptions } from 'core/util/index'
+import { mergeOptions, isPlainObject, noop } from 'core/util/index'
+import Watcher from 'core/observer/watcher'
 import { initProxy } from 'core/instance/proxy'
-import { initState } from 'core/instance/state'
+import { initState, getData } from 'core/instance/state'
 import { initRender } from 'core/instance/render'
 import { initEvents } from 'core/instance/events'
 import { initProvide, initInjections } from 'core/instance/inject'
-import { initLifecycle, mountComponent, callHook } from 'core/instance/lifecycle'
+import { initLifecycle, callHook } from 'core/instance/lifecycle'
 import { initInternalComponent, resolveConstructorOptions } from 'core/instance/init'
 import { registerComponentHook, updateComponentData } from '../../util/index'
 
@@ -55,8 +56,25 @@ function initVirtualComponent (options: Object = {}) {
   initProvide(vm) // resolve provide after data/props
   callHook(vm, 'created')
 
+  // send initial data to native
+  const data = vm.$options.data
+  const params = typeof data === 'function'
+    ? getData(data, vm)
+    : data || {}
+  if (isPlainObject(params)) {
+    updateComponentData(componentId, params)
+  }
+
   registerComponentHook(componentId, 'lifecycle', 'attach', () => {
-    mountComponent(vm)
+    callHook(vm, 'beforeMount')
+
+    const updateComponent = () => {
+      vm._update(vm._vnode, false)
+    }
+    new Watcher(vm, updateComponent, noop, null, true)
+
+    vm._isMounted = true
+    callHook(vm, 'mounted')
   })
 
   registerComponentHook(componentId, 'lifecycle', 'detach', () => {
@@ -65,25 +83,53 @@ function initVirtualComponent (options: Object = {}) {
 }
 
 // override Vue.prototype._update
-function updateVirtualComponent (vnode: VNode, hydrating?: boolean) {
-  // TODO
-  updateComponentData(this.$options.componentId, {})
+function updateVirtualComponent (vnode?: VNode) {
+  const vm: Component = this
+  const componentId = vm.$options.componentId
+  if (vm._isMounted) {
+    callHook(vm, 'beforeUpdate')
+  }
+  vm._vnode = vnode
+  if (vm._isMounted && componentId) {
+    // TODO: data should be filtered and without bindings
+    const data = Object.assign({}, vm._data)
+    updateComponentData(componentId, data, () => {
+      callHook(vm, 'updated')
+    })
+  }
 }
 
 // listening on native callback
 export function resolveVirtualComponent (vnode: MountedComponentVNode): VNode {
   const BaseCtor = vnode.componentOptions.Ctor
   const VirtualComponent = BaseCtor.extend({})
+  const cid = VirtualComponent.cid
   VirtualComponent.prototype._init = initVirtualComponent
   VirtualComponent.prototype._update = updateVirtualComponent
 
   vnode.componentOptions.Ctor = BaseCtor.extend({
     beforeCreate () {
-      registerComponentHook(VirtualComponent.cid, 'lifecycle', 'create', componentId => {
+      // const vm: Component = this
+
+      // TODO: listen on all events and dispatch them to the
+      // corresponding virtual components according to the componentId.
+      // vm._virtualComponents = {}
+      const createVirtualComponent = (componentId, propsData) => {
         // create virtual component
-        const options = { componentId }
-        return new VirtualComponent(options)
-      })
+        // const subVm =
+        new VirtualComponent({
+          componentId,
+          propsData
+        })
+        // if (vm._virtualComponents) {
+        //   vm._virtualComponents[componentId] = subVm
+        // }
+      }
+
+      registerComponentHook(cid, 'lifecycle', 'create', createVirtualComponent)
+    },
+    beforeDestroy () {
+      delete this._virtualComponents
     }
   })
 }

+ 6 - 2
src/platforms/weex/util/index.js

@@ -70,13 +70,17 @@ export function registerComponentHook (
 }
 
 // Updates the state of the component to weex native render engine.
-export function updateComponentData (componentId: string, newData: Object) {
+export function updateComponentData (
+  componentId: string,
+  newData: Object | void,
+  callback?: Function
+) {
   if (!document || !document.taskCenter) {
     warn(`Can't find available "document" or "taskCenter".`)
     return
   }
   if (typeof document.taskCenter.updateData === 'function') {
-    return document.taskCenter.updateData(componentId, newData)
+    return document.taskCenter.updateData(componentId, newData, callback)
   }
   warn(`Failed to update component data (${componentId}).`)
 }

+ 66 - 17
test/weex/cases/cases.spec.js

@@ -137,29 +137,45 @@ describe('Usage', () => {
         const id = String(Date.now() * Math.random())
         const instance = createInstance(id, code)
         expect(tasks.length).toEqual(3)
-        tasks.length = 0
-        instance.$triggerHook(2, 'create', ['component-1'])
-        instance.$triggerHook(2, 'create', ['component-2'])
-        instance.$triggerHook('component-1', 'attach')
-        instance.$triggerHook('component-2', 'attach')
-        expect(tasks.length).toEqual(2)
-        expect(tasks[0].method).toEqual('updateComponentData')
-        // expect(tasks[0].args).toEqual([{ count: 42 }])
-        expect(tasks[1].method).toEqual('updateComponentData')
-        // expect(tasks[1].args).toEqual([{ count: 42 }])
         setTimeout(() => {
+          // check the render results
           const target = readObject('recycle-list/components/stateful.vdom.js')
           expect(getRoot(instance)).toEqual(target)
-          const event = getEvents(instance)[0]
           tasks.length = 0
-          fireEvent(instance, event.ref, event.type, {})
+
+          // trigger component hooks
+          instance.$triggerHook(
+            2, // cid of the virtual component template
+            'create', // lifecycle hook name
+
+            // arguments for the callback
+            [
+              'x-1', // componentId of the virtual component
+              { start: 3 } // propsData of the virtual component
+            ]
+          )
+          instance.$triggerHook(2, 'create', ['x-2', { start: 11 }])
+
+          // the state (_data) of the virtual component should be sent to native
+          expect(tasks.length).toEqual(2)
+          expect(tasks[0].method).toEqual('updateComponentData')
+          expect(tasks[0].args).toEqual(['x-1', { count: 6 }, ''])
+          expect(tasks[1].method).toEqual('updateComponentData')
+          expect(tasks[1].args).toEqual(['x-2', { count: 22 }, ''])
+
+          instance.$triggerHook('x-1', 'attach')
+          instance.$triggerHook('x-2', 'attach')
+          tasks.length = 0
+
+          // simulate a click event
+          // the event will be caught by the virtual component template and
+          // should be dispatched to virtual component according to the componentId
+          const event = getEvents(instance)[0]
+          fireEvent(instance, event.ref, 'click', { componentId: 'x-1' })
           setTimeout(() => {
             // expect(tasks.length).toEqual(1)
-            // expect(tasks[0]).toEqual({
-            //   module: 'dom',
-            //   method: 'updateComponentData',
-            //   args: [{ count: 43 }]
-            // })
+            // expect(tasks[0].method).toEqual('updateComponentData')
+            // expect(tasks[0].args).toEqual([{ count: 7 }])
             instance.$destroy()
             resetTaskHook()
             done()
@@ -168,6 +184,39 @@ describe('Usage', () => {
       }).catch(done.fail)
     })
 
+    // it('component lifecycle', done => {
+    //   global.__lifecycles = []
+    //   compileWithDeps('recycle-list/components/stateful-lifecycle.vue', [{
+    //     name: 'lifecycle',
+    //     path: 'recycle-list/components/lifecycle.vue'
+    //   }]).then(code => {
+    //     const id = String(Date.now() * Math.random())
+    //     const instance = createInstance(id, code)
+    //     setTimeout(() => {
+    //       const target = readObject('recycle-list/components/stateful-lifecycle.vdom.js')
+    //       expect(getRoot(instance)).toEqual(target)
+
+    //       instance.$triggerHook(2, 'create', ['y-1'])
+    //       instance.$triggerHook('y-1', 'attach')
+    //       instance.$triggerHook('y-1', 'detach')
+    //       expect(global.__lifecycles).toEqual([
+    //         'beforeCreate undefined',
+    //         'created 0',
+    //         'beforeMount 1',
+    //         'mounted 1',
+    //         'beforeUpdate 2',
+    //         'updated 2',
+    //         'beforeDestroy 2',
+    //         'destroyed 2'
+    //       ])
+
+    //       delete global.__lifecycles
+    //       instance.$destroy()
+    //       done()
+    //     }, 50)
+    //   }).catch(done.fail)
+    // })
+
     it('stateful component with v-model', done => {
       compileWithDeps('recycle-list/components/stateful-v-model.vue', [{
         name: 'editor',

+ 1 - 1
test/weex/cases/recycle-list/components/counter.vue

@@ -10,7 +10,7 @@
     props: ['start'],
     data () {
       return {
-        count: parseInt(this.start, 10) || 42
+        count: parseInt(this.start, 10) * 2 || 42
       }
     },
     methods: {

+ 39 - 0
test/weex/cases/recycle-list/components/lifecycle.vue

@@ -0,0 +1,39 @@
+<template recyclable="true">
+  <div>
+    <text>{{number}}</text>
+  </div>
+</template>
+
+<script>
+  module.exports = {
+    data () {
+      return { number: 0 }
+    },
+    beforeCreate () {
+      try { __lifecycles.push('beforeCreate ' + this.number) } catch (e) {}
+    },
+    created () {
+      try { __lifecycles.push('created ' + this.number) } catch (e) {}
+      this.number++
+    },
+    beforeMount () {
+      try { __lifecycles.push('beforeMount ' + this.number) } catch (e) {}
+    },
+    mounted () {
+      try { __lifecycles.push('mounted ' + this.number) } catch (e) {}
+      this.number++
+    },
+    beforeUpdate () {
+      try { __lifecycles.push('beforeUpdate ' + this.number) } catch (e) {}
+    },
+    updated () {
+      try { __lifecycles.push('updated ' + this.number) } catch (e) {}
+    },
+    beforeDestroy () {
+      try { __lifecycles.push('beforeDestroy ' + this.number) } catch (e) {}
+    },
+    destroyed () {
+      try { __lifecycles.push('destroyed ' + this.number) } catch (e) {}
+    }
+  }
+</script>

+ 29 - 0
test/weex/cases/recycle-list/components/stateful-lifecycle.vdom.js

@@ -0,0 +1,29 @@
+({
+  type: 'recycle-list',
+  attr: {
+    append: 'tree',
+    listData: [
+      { type: 'X' },
+      { type: 'X' }
+    ],
+    templateKey: 'type',
+    alias: 'item'
+  },
+  children: [{
+    type: 'cell-slot',
+    attr: { append: 'tree', templateType: 'X' },
+    children: [{
+      type: 'div',
+      attr: {
+        '@isComponentRoot': true,
+        '@componentProps': {}
+      },
+      children: [{
+        type: 'text',
+        attr: {
+          value: { '@binding': 'number' }
+        }
+      }]
+    }]
+  }]
+})

+ 21 - 0
test/weex/cases/recycle-list/components/stateful-lifecycle.vue

@@ -0,0 +1,21 @@
+<template>
+  <recycle-list :list-data="longList" template-key="type" alias="item">
+    <cell-slot template-type="X">
+      <lifecycle></lifecycle>
+    </cell-slot>
+  </recycle-list>
+</template>
+
+<script>
+  // require('./lifecycle.vue')
+  module.exports = {
+    data () {
+      return {
+        longList: [
+          { type: 'X' },
+          { type: 'X' }
+        ]
+      }
+    }
+  }
+</script>

+ 2 - 1
test/weex/runtime/framework.spec.js

@@ -36,7 +36,7 @@ describe('framework APIs', () => {
       type: 'div',
       children: [{
         type: 'text',
-        attr: { value: '{"bundleUrl":"http://example.com/","a":1,"b":2,"env":{}}' }
+        attr: { value: '{"bundleUrl":"http://example.com/","a":1,"b":2,"env":{},"bundleType":"Vue"}' }
       }]
     })
   })
@@ -170,6 +170,7 @@ describe('framework APIs', () => {
     `, { bundleUrl: 'http://whatever.com/x.js' })
     expect(JSON.parse(getRoot(instance).children[0].attr.value)).toEqual({
       bundleUrl: 'http://whatever.com/x.js',
+      bundleType: 'Vue',
       env: {
         weexVersion: '0.10.0',
         platform: 'Node.js'