Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

<component v-for> problem #2424

Closed
jiangfengming opened this issue Mar 4, 2016 · 20 comments
Closed

<component v-for> problem #2424

jiangfengming opened this issue Mar 4, 2016 · 20 comments
Labels

Comments

@jiangfengming
Copy link

Vue.js version

1.0.17

Reproduction Link

https://jsfiddle.net/fenivana/7jcoogfc/

Steps to reproduce

Click the button in jsfiddle demo

What is Expected?

The component rendered without error.

What is actually happening?

Uncaught TypeError: Cannot read property 'reused' of undefined (vue.js:4116)

@oakinogundeji
Copy link

Hi fenivana,

I've looked at the code in the reproduction link, i see what you mean about the error which is displayed in the console. I copied the code and set it up exactly on my system and it rendered the component without errors. Furthermore, if you examine the console output, you'll notice that the log messages for 'created' and 'check' are repeated before the error is thrown. On my system, no such thing happens... it simply creates the component and logs the messages in sequence as expected i.e. 'created' followed by 'check'.

My thoughts are that to understand what's really happening we need to deconstruct your code. Taking it from the top..

<button @click="a++">click</button> <component v-for="item in comps" :is="item.name" :option="item">

What exactly are you trying to achieve with this template? What i can see is that..

  1. When the button is clicked, increment the 'a' property.
  2. You are trying to render a list using 'v-for' however there are 3 things which i find 'confusing'..
    a) From the docs, the 'component' tag is a special tag used for dynamically rendering components which share a single 'mount point', and the ':is' attribute is a special attribute binding which selects the component to render based on some property of the 'parent' Vue vm. Since you've chosen to bind the parent vm to the 'html' element, the above structure is valid. However, this raises the 2nd issue.
    b) 'v-for' directive is used for rendering lists, it can't be used to render a list of dynamic components which is what the code in the element is essentially saying. Furthermore the ':is' binding is bound to 'item' which i assume is the label for an element of the 'comps' array... but from the docs the 'target' for the ':is' binding has to be some property on the parent vm... thus your ':is=item.name' would be correct if there actually existed an 'item' data property on the parent vm which itself had a 'name' property. But since such a property does not exist, the value for a variable in javascript which is 'declared' but not instantiated is 'undefined'. Thus the value of 'item' is 'undefined'.
    c) Finally, the ':option" props binding which you are implementing also suffers from the same problem as discussed above i.e. the 'target' value of the binding is 'item' and no such property exists on the parent vm. Thus again, the value of 'option' is 'undefined'

With the preceding analysis, i can say that what is happening is the following:

  1. The 'a' property on the parent vm is initially falsy (i.e. the value is 0), on 'click' the value becomes truthy i.e. 1 and above.
  2. When the value of 'a' becomes truthy, 'this.comps' is set to 'foo' as contained in the code below:
    watch: { a: function() { this.comps = [{ name: 'foo' }]; } }
  3. Since 'this.comps' is now set to 'foo' the 'component' tag examines the parent vm for any registered component with the label 'foo'... and of course it finds such a component as contained in the code below:

components: {
foo: {
template: '

foo
',

  props: ['option'],

  created: function() {
    console.log('created')
    this.check();
  },

  methods: {
    check: function() {
      console.log('check');
      this.$set('option.bar', true);
    }
  }
}

}

  1. When the 'foo' component is rendered, the 'template' to be inserted simply renders the 'div' with text content of 'foo' as follows:
    template: '<div>foo</div>',
  2. When the component is rendered, the 'created' lifecycle hook is triggered and 3 things happen in sequence:
    a) 'created' is logged.
    b) 'this.check()' method is triggered,
    c) 'check' is logged,
  3. 'this.$set('option.bar', true);' is triggered which causes the component to try to create a 'bar' property on the 'option' prop and to set that property to 'true'. This throws an error for the following reasons:
    a) The 'option' prop receives it's value from the parent vm, we have already discovered that the value will be 'undefined' since the 'item' property of the parent vm which should give 'option' a value is non existent.
    b) Also, the syntax used to set this 'props' is a one way binding syntax from the parent vm to the child component. This means that 'option' can't be mutated by the 'foo' component.
    c) Thus when 'this.$set('option.bar', true);' is triggered, it tries to cause Vue to perform an operation (i.e. dynamically setting a previously non existent property on the 'foo' component) on an 'undefined' property (i.e. 'option').

Wheeeew...

I may be wrong in my analysis, but the preceding is what i think is causing the error to be thrown. And maybe because of the nature of 'jsfiddle' the 'issues' with the code are not ignored.

FYI my attempt to replicate the issue on my system is..:
`

<title>Component v-for error</title> <script src="vue.js"></script> click <script src="app.js"></script> ` Maybe something about my code is more fault tolerant than 'jsfiddle' and so the 'issues' are simply ignored and the component rendered as expected.

By the way, what exactly are you trying to achieve? Are you trying to dynamicayy generate a set of 'divs' when the button is clicked?

I hope my attempt to explain what's happening is helpful.

Regards.

@prog-rajkamal
Copy link

@fenivana
I changed created to ready and it works

In my opinion, the error was caused by infinite loop.

1)this.comps = changes comps and causes re-rendering of <component>
2) component is created with item as reactive data
3) in created of foo, check() is called
4)in check(), this.$set('option.bar', true); changes item
5) item changed, so go to step 2 ..... and you have infinite loop

how the infinite loop ends up with that error? i have no idea.

@oakinogundeji
Copy link

Ahhh... i see. Great then.

May i ask why you're using 'v-for' with the 'component' tag? My understanding is that it shoudn't work. I'd appreciate some insight.

Regards.

@simplesmiler
Copy link
Member

There is definitely a bug somewhere. Somehow during vFor.diff after button was clicked, this.frags.length gets to be 1 with this.frags[0] being undefined.
Also if bar is defined on the items beforehand, then the issue goes away.
cc @yyx990803.

@yyx990803 yyx990803 added the bug label Mar 4, 2016
@simplesmiler
Copy link
Member

From diff:

  1. var frags = this.frags = new Array(data.length)
  2. frag = this.create(value, alias, i, key)
  3. frags[i] = frag

During initialization, [2] asynchronously recursively calls diff, which lets [3] to set the fragment.

diff (vue.js:4048)   <-- IMPORTANT
update (vue.js:4026)
Directive._bind._update (vue.js:8000)
Watcher.run (vue.js:3348)
runBatcherQueue (vue.js:3081)
flushBatcherQueue (vue.js:3057)
nextTickHandler (vue.js:438)

== Mutation (async) ==

timerFunc (vue.js:452)
(anonymous function) (vue.js:468)
pushWatcher (vue.js:3120)
Watcher.update (vue.js:3311)   <-- SAME STACK UP TO THIS POINT
Dep.notify (vue.js:2043)
set (vue.js:27)
setPath (vue.js:2823)
(anonymous function) (vue.js:2969)
Vue.$set (vue.js:8582)
Vue.components.foo.created ((index):74)
Vue._callHook (vue.js:7899)
Vue._init (vue.js:2495)
VueComponent (VM8358:2)
build (vue.js:5722)
mountComponent (vue.js:5639)
(anonymous function) (vue.js:5604)
(anonymous function) (vue.js:5619)
cb (vue.js:367)
Vue._resolveComponent (vue.js:8535)
resolveComponent (vue.js:5621)
setComponent (vue.js:5603)
update (vue.js:5577)
Directive._bind (vue.js:8023)
linkAndCapture (vue.js:6599)
compositeLinkFn (vue.js:6575)
Fragment (vue.js:3742)
FragmentFactory.create (vue.js:3959)
create (vue.js:4188)   <-- IMPORTANT
diff (vue.js:4091)   <-- IMPORTANT

After button is pressed, [2] synchronously recursively calls diff, after [1] has set the length but before [3] has set the elements.

diff (vue.js:4047)   <-- IMPORTANT
update (vue.js:4026)
Directive._bind._update (vue.js:8000)
Watcher.run (vue.js:3348)
pushWatcher (vue.js:3110)
Watcher.update (vue.js:3311)   <-- SAME STACK UP TO THIS POINT
Dep.notify (vue.js:2043)
set (vue.js:27)
setPath (vue.js:2823)
(anonymous function) (vue.js:2969)
Vue.$set (vue.js:8582)
Vue.components.foo.created ((index):74)
Vue._callHook (vue.js:7899)
Vue._init (vue.js:2495)
VueComponent (VM8358:2)
build (vue.js:5722)
mountComponent (vue.js:5639)
(anonymous function) (vue.js:5604)
(anonymous function) (vue.js:5619)
cb (vue.js:367)
Vue._resolveComponent (vue.js:8535)
resolveComponent (vue.js:5621)
setComponent (vue.js:5603)
update (vue.js:5577)
Directive._bind (vue.js:8023)
linkAndCapture (vue.js:6599)
compositeLinkFn (vue.js:6575)
Fragment (vue.js:3742)
FragmentFactory.create (vue.js:3959)
create (vue.js:4188)   <-- IMPORTANT
diff (vue.js:4091)   <-- IMPORTANT

@oakinogundeji
Copy link

@simplesmiler, @yyx990803
I feel what you're saying, but i still want to know how it's possible to use 'v-for' on "" if " is specifically to be used for rendering dynamic components of a parent vm. It's almost like saying we can use 'v-for' on a "' tag.

I'd really appreciate an explanation.

Regards.

@yyx990803
Copy link
Member

@oakinogundeji <component v-for="item in items" :is="item.type"> is a common technique for rendering a list of different components driven by an array. You wouldn't do that for <router-view> because it doesn't make sense to have multiple <router-view>s next to each other.

@simplesmiler
Copy link
Member

@yyx990803 would not the solution just be the following?

//var frags = this.frags = new Array(data.length)
var frags = new Array(data.length)
// after all fragments are set
this.frags = frags

@yyx990803
Copy link
Member

This is actually an async queue scheduling issue - the change was triggered from a watcher, which leads to it being applied synchronously before the diff finishes - basically, triggering a new diff in the middle of an ongoing diff, thus messing up all the intermediate state.

@yyx990803
Copy link
Member

As a side note, I'd like to point out that mutating object props is not recommended practice...

@simplesmiler
Copy link
Member

@yyx990803 I see. I'm still learning how Vue internals are supposed to work.

@oakinogundeji
Copy link

@yyx990803 I know this issue has been resolved but i have a few questions i'd like clarification on.

  1. In your response to me, you said the use of is a common technique for rendering a list of different components driven by an array. While i do not dispute what you say (after all you really do know best) i'd like to point out that this is not mentioned in the recent documentation (http://vuejs.org/guide/components.html).

My problem isn't with the 'v-for' or ':is' directives in themselves, rather my problem is usage of these directives with the 'component' tag. According to the contents of the docs, the 'component' tag is reserved for use a s a mount point to dynamically switch between multiple components which are bound to the ':is' attribute of the parent vm.

I would like clarification on this issue since i believe that a tool as popular as Vue shouldn't throw these kind of surprises based on the docs. Besides, while i'm relatively new to Vue.js, it is my tool of choice for building client side apps and i promote it in my writings (https://medium.com/@nohkachi).

  1. I'd really like to know how simplesmiler was able to get the info he quoted i.e. 'diff' etc. Is there some special debugger mode i can enable so i can get such insight?

Regards,

PS
I hope to be able to contribute much more to Vue.js, i have some tutorials and articles i believe may benefit the community. How can i contribute?

@yyx990803
Copy link
Member

@oakinogundeji you're right - this should be in the docs. The phrasing in the current doc about <component> didn't imply that it cannot work in conjunction with v-for. The <component> tag is just a generic placeholder (same as all custom elements merely serving as placeholder for the template of the components). You can in fact use any directive on it. For example, you can do this:

<custom-component v-for="item in items"></custom-component>

Which means you can do this too:

<component v-for="item in items" :is="item.type"></component>

@oakinogundeji
Copy link

@yyx990803 Thanks Evan, i appreciate the clarification. If i understand you correctly, you are saying that while the 'component' tag has a primary function of serving as a mount point for dynamically rendering child components, we could also use it to generate a 'list' of components just as we would use the 'v-for' directive on any HTML element.

I'd appreciate if you clarified the significance of ':is="item.type" ', what exactly does the '.type' property signify? Are we supposed to assign the elements in the list a 'type' property?

Thanks for the response i appreciate.

Regards.

@oakinogundeji
Copy link

@simplesmiler
Hi, please how did you access 'vFor.diff' ? I was impressed at the amount of insight you were able to gain. I'd appreciate some pointers.

Regards.

@yyx990803
Copy link
Member

@oakinogundeji probably easier to understand with a demo: https://jsfiddle.net/9c4nwt9a/

If you are interested in Vue internals, you can enable Vue.config.debug = true which will give you stack traces for Vue warnings. You can then add breakpoints in Chrome devtools.

@oakinogundeji
Copy link

@yyx990803 Thanks man, really appreciate all your help. Yes i am interested in Vue internals and everything Vue. I really appreciate what you've done with it.

May i submit my material for your review? It may be helpful for other beginners like me :-)

Regards.

@simplesmiler
Copy link
Member

@oakinogundeji as Evan said, it was Chrome DevTools, Vue source code and some guesswork.

@oakinogundeji
Copy link

@simplesmiler Thanks man.

Regards.

@xubaoshi
Copy link

xubaoshi commented May 11, 2016

@yyx990803 我使用<component :is="currentView" :data="data"></component>,currentView为对应的动态组件 ,data为异步获取的数据,请问我通过什么方式能知道对应的动态组件已dom加载完毕,现在我在对应动态组件中 添加ready() this.$nextTick监听,但依旧无法获取动态组件,请问有什么方法吗??麻烦了。。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants