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.
This commit is contained in:
Hanks 2017-12-19 05:16:03 +08:00 committed by Evan You
parent d544d052a9
commit 661bfe552e
13 changed files with 229 additions and 37 deletions

View File

@ -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

View File

@ -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 };

6
package-lock.json generated
View File

@ -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": {

View File

@ -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": {

View File

@ -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) {

View File

@ -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
}
})
}

View File

@ -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}).`)
}

View File

@ -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',

View File

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

View File

@ -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>

View File

@ -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' }
}
}]
}]
}]
})

View File

@ -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>

View File

@ -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'