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; _hasHookEvent: boolean;
_provided: ?Object; _provided: ?Object;
_inlineComputed: ?{ [key: string]: Watcher }; // inline computed watchers for literal props _inlineComputed: ?{ [key: string]: Watcher }; // inline computed watchers for literal props
// _virtualComponents?: { [key: string]: Component };
// private methods // private methods

View File

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

6
package-lock.json generated
View File

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

View File

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

View File

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

View File

@ -2,13 +2,14 @@
// https://github.com/Hanks10100/weex-native-directive/tree/master/component // 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 { 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 { initRender } from 'core/instance/render'
import { initEvents } from 'core/instance/events' import { initEvents } from 'core/instance/events'
import { initProvide, initInjections } from 'core/instance/inject' 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 { initInternalComponent, resolveConstructorOptions } from 'core/instance/init'
import { registerComponentHook, updateComponentData } from '../../util/index' import { registerComponentHook, updateComponentData } from '../../util/index'
@ -55,8 +56,25 @@ function initVirtualComponent (options: Object = {}) {
initProvide(vm) // resolve provide after data/props initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') 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', () => { 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', () => { registerComponentHook(componentId, 'lifecycle', 'detach', () => {
@ -65,25 +83,53 @@ function initVirtualComponent (options: Object = {}) {
} }
// override Vue.prototype._update // override Vue.prototype._update
function updateVirtualComponent (vnode: VNode, hydrating?: boolean) { function updateVirtualComponent (vnode?: VNode) {
// TODO const vm: Component = this
updateComponentData(this.$options.componentId, {}) 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 // listening on native callback
export function resolveVirtualComponent (vnode: MountedComponentVNode): VNode { export function resolveVirtualComponent (vnode: MountedComponentVNode): VNode {
const BaseCtor = vnode.componentOptions.Ctor const BaseCtor = vnode.componentOptions.Ctor
const VirtualComponent = BaseCtor.extend({}) const VirtualComponent = BaseCtor.extend({})
const cid = VirtualComponent.cid
VirtualComponent.prototype._init = initVirtualComponent VirtualComponent.prototype._init = initVirtualComponent
VirtualComponent.prototype._update = updateVirtualComponent VirtualComponent.prototype._update = updateVirtualComponent
vnode.componentOptions.Ctor = BaseCtor.extend({ vnode.componentOptions.Ctor = BaseCtor.extend({
beforeCreate () { 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 // create virtual component
const options = { componentId } // const subVm =
return new VirtualComponent(options) 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. // 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) { if (!document || !document.taskCenter) {
warn(`Can't find available "document" or "taskCenter".`) warn(`Can't find available "document" or "taskCenter".`)
return return
} }
if (typeof document.taskCenter.updateData === 'function') { 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}).`) warn(`Failed to update component data (${componentId}).`)
} }

View File

@ -137,29 +137,45 @@ describe('Usage', () => {
const id = String(Date.now() * Math.random()) const id = String(Date.now() * Math.random())
const instance = createInstance(id, code) const instance = createInstance(id, code)
expect(tasks.length).toEqual(3) 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(() => { setTimeout(() => {
// check the render results
const target = readObject('recycle-list/components/stateful.vdom.js') const target = readObject('recycle-list/components/stateful.vdom.js')
expect(getRoot(instance)).toEqual(target) expect(getRoot(instance)).toEqual(target)
const event = getEvents(instance)[0]
tasks.length = 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(() => { setTimeout(() => {
// expect(tasks.length).toEqual(1) // expect(tasks.length).toEqual(1)
// expect(tasks[0]).toEqual({ // expect(tasks[0].method).toEqual('updateComponentData')
// module: 'dom', // expect(tasks[0].args).toEqual([{ count: 7 }])
// method: 'updateComponentData',
// args: [{ count: 43 }]
// })
instance.$destroy() instance.$destroy()
resetTaskHook() resetTaskHook()
done() done()
@ -168,6 +184,39 @@ describe('Usage', () => {
}).catch(done.fail) }).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 => { it('stateful component with v-model', done => {
compileWithDeps('recycle-list/components/stateful-v-model.vue', [{ compileWithDeps('recycle-list/components/stateful-v-model.vue', [{
name: 'editor', name: 'editor',

View File

@ -10,7 +10,7 @@
props: ['start'], props: ['start'],
data () { data () {
return { return {
count: parseInt(this.start, 10) || 42 count: parseInt(this.start, 10) * 2 || 42
} }
}, },
methods: { 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', type: 'div',
children: [{ children: [{
type: 'text', 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' }) `, { bundleUrl: 'http://whatever.com/x.js' })
expect(JSON.parse(getRoot(instance).children[0].attr.value)).toEqual({ expect(JSON.parse(getRoot(instance).children[0].attr.value)).toEqual({
bundleUrl: 'http://whatever.com/x.js', bundleUrl: 'http://whatever.com/x.js',
bundleType: 'Vue',
env: { env: {
weexVersion: '0.10.0', weexVersion: '0.10.0',
platform: 'Node.js' platform: 'Node.js'