Vue Learning Summary

✍🏼 Written on Jun 1, 2016   
❗️ Note: it has been days since this article was written, please be aware of its timeliness

Preface

This article begins with the “Instance” section from the Vue 2.0 Official Documentation], exploring some usage methods of the Vue API and the principles behind how Vue implements certain functionalities. Additionally, it includes personal usage experiences and, from my limited perspective, an analysis of why Vue is designed this way—though this may seem presumptuous. As I am relatively new to Vue, any inaccuracies are kindly requested to be pointed out, and I appreciate your understanding in advance.

Note: Basic knowledge is skipped directly; I will only mention points I deem necessary.

Vue Instance

Every Vue.js application is launched by creating a root Vue instance via a constructor. This means that all data on a page should be maintained solely by this single instance, with the original data sources being emitted and received exclusively by the root instance for unified management. The root instance then distributes data via props or listens for data through events. Child components only need to watch/computed data changes and update promptly.

The documentation states: All Vue.js components are essentially extended Vue instances. The correct interpretation of this is that you can use the same methods and lifecycle hooks on components as you would on instances, except for data.

In components, data must be a function because components are reusable, so each invocation of a component must generate its own data.

Data proxy refers to the fact that a Vue instance proxies all properties in its data object, while the instance property $data represents the data attribute itself, distinguishing it from the proxied data.

In other words, if a vm’s data property is {a: 'xheldon'}, then vm.a equals 'xheldon', while vm.$data is {a: 'xheldon'}.

A component is essentially an (extended) Vue instance. Here’s a simple verification:

A list.vue component (template and style omitted):

1
2
3
4
5
6
7
8
import Vue from 'vue'
export default{
data(){
console.log(this instanceof Vue);//true
return {}
}
name: 'com-list'
}

props vs data

When initializing a component, properties on props, data, and methods in computed are all bound to the Vue instance. However, properties on props have higher priority than those with the same name on data. Here’s the verification:

1
2
3
4
5
6
7
8
9
10
11
export default {
data() {
console.log('first:', this); //list 的实例
return {
a: 'a', //属性 a 代表的值挂在 data 上, 但是被下面的 prop 属性同名覆盖(查看上面控制台输出的内容即可)
d: 'd',
};
},
props: ['a'], //和 data 中的 a 属性同名, 因此来自父级的数据将 data 中的同名属性 a 上的数据覆盖.(注:父级是根组件, 挂载在一个实例上)
name: 'com-list',
};

The result shows a warning (not an error, rendering is unaffected):

1
[Vue warn]: The data property "a" is already declared as a prop. Use prop default value instead.

props VS data

Meanwhile, the function names returned by computed can duplicate those of properties on data without any warnings. However, once duplicated, during initialization, the console shows that _data sets the getter and setter for the duplicated property after _computedWatcher (whether this is the reason remains to be confirmed after further research, so I’ll leave it here for now and update this article later). This leads to the property being overwritten. For related principles, see this introduction].

They are all bound to the instance properties during initialization. Duplicate computed properties are overwritten, but Vue devtool still displays them correctly.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default {
data() {
console.log('first:', this); //list 的实例
return {
ab: 'a', //属性 a 代表的值挂在 data 上, 但是被下面的 prop 属性同名覆盖(查看上面控制台输出的内容即可)
};
},
computed: {
ab() {
return 'computed a'; //方法名跟 data 上的 a 属性同名, 因此console 出来的 this 不会出现它的值, 用花括号输出的时候也是输出的 data 上的同名属性
},
f() {
return 'computed f'; //方法名没有重复的, 因此 console 出来的 this 会有它的同名属性, 且值为 'computed f', 我们提供的 function 被作为该属性的 getter(计算属性默认只有 getter, 可以手动添加 setter)
},
},
name: 'com-list',
};

computed VS data

Vue devtool displays them correctly:

computed VS data

If the method names in computed do not duplicate the property names in data:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default {
data() {
console.log('first:', this); //list 的实例
return {
a: 'a', //属性 a 代表的值挂在 data 上, 但是被下面的 prop 属性同名覆盖(查看上面控制台输出的内容即可)
};
},
computed: {
ab() {
return 'computed a'; //方法名跟 data 上的 a 属性同名, 因此 console 出来的 this 不会出现它的值, 但 Vue devtool 控制台正确显示了出来, 用花括号输出的时候也是其返回值 computed a.
},
f() {
return 'computed f'; //方法名没有重复的, 因此 console 出来的 this 会有它的同名属性, 且值为 'computed f', 我们提供的 function 被作为该属性的 getter
},
},
name: 'com-list',
};

computed VS data

Additionally, the difference between methods and computed methods, aside from the latter having caching while the former does not (i.e., the latter won’t recalculate unless its dependent reactive data changes), is that if both are intended to return interpolated values, methods in methods are invoked as functionName(), whereas methods in computed are referenced as functionName—meaning the former requires executing the function, while the latter does not.

The reason for this is that the methods we write in computed properties are treated as properties mounted on the Vue instance, and the functions we provide serve as the getter for these properties.

In contrast, methods are simply functions, so whether they are referenced in interpolations or as event handlers, they must be called using ().

Thus, when an actual function is needed—here, if a function call is required, computed must return a function, not just a value, because parameters need to be passed—methods take precedence.

Below is an alternative version of a todo list different from the official documentation:

Template:

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>
<input type="text" @keyup.enter="addToList" v-model="todotext" />
<ul>
{% raw %}
<li v-for="(value, key) in todolist">
{{key}}:{{value}}<button @click="deletetodo(key)">X</button>
</li>
{% endraw %}
</ul>
</div>
</template>

Logic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default {
data() {
return {
todolist: [],
todotext: '',
};
},
methods: {
addToList() {
this.todolist.push(this.todotext);
},
deletetodo(key) {
console.log(arguments);
this.todolist.splice(key, 1);
},
} /*,
computed:{
deletetodo(key){//不接收参数
console.log(arguments);
this.todolist.splice(key, 1);
}
}*/,
};

In this example, if this template is used, a parameter must be passed to delete the current li when the button is clicked. Therefore, methods must be used in this case.

When using computed, parameters are not accepted (because it is a getter), even if it returns a function:

1
2
3
4
5
6
7
8
computed:{
deletetodo(key){
return function(){
console.log(arguments);//控制台输出当前组件实例
this.todolist.splice(key, 1);//报错
}
}
}

Note: If there are functions with the same name in both, the function in computed takes precedence over the one in methods (due to the order in which getter and setter are processed; refer to the source code for details). This applies to both interpolations and event bindings. Here’s the verification:

Interpolation reference test:

Template:

1
2
3
{%raw%}
<div>{{a()}}</div>
{% endraw%}
1
2
3
4
5
6
7
8
9
10
11
12
methods:{
a(){
return 'im from methods'
}
},
computed:{
a(){
return function(){
return 'im from computed'
}
}
}

The output function is im from computed. If computed does not return a function, the interpolation reference using only {{a}} will obviously output the value from computed (verification omitted here).

For event binding:

When calling using inline handler methods:

1
2
3
{%raw%}
<div @click="a()">click me</div>
{% endraw%}

Logic:

1
2
3
4
5
6
7
8
9
10
11
12
methods:{
a(){
console.log('methods')
}
},
computed:{
a(){
return function(){//computed 返回一个函数
console.log('computed')
}
}
}

Outputs computed.

If using method event handlers, the result is the same:

1
2
3
{%raw%}
<div @click="a">click me</div>
//注意此处不带括号 {% endraw%}

Logic:

1
2
3
4
5
6
7
8
9
10
11
12
methods:{
a(){
console.log('methods')
}
},
computed:{
a(){
return function(){//computed 返回一个函数
console.log('computed')
}
}
}

Outputs computed.

Note that for event binding, computed must always return a function.

Thus, the order of precedence is:

props > data > computed > methods

I suspect the reference priority for this.xxx in computed and methods follows the same order. Those curious can verify this.

Moreover, it can be observed that in both cases, methods do not need to return a function, whereas computed must return a function and does not accept parameters (since it is a getter). In summary, event handling is best done with methods, while data binding/interpolation is best handled with computed (due to caching).

Additionally, when binding events with methods, using () or omitting it has the same effect—both will execute the function. The differences are as follows:

  1. Statements with () are called inline statements, which fall into two cases: native events and custom events. Both cases can pass parameters. If the parameter list is empty, the default parameter arguments is also empty, meaning there is no default parameter. If triggered by a native event like input/click, a special $event parameter can be passed as the native event handler. If triggered by a custom event, the parameters of the event handler still depend on the values actually passed to it, and there is no special $event object available for custom functions. Parameters passed when triggering a custom event via $emit will be ignored.

  2. Statements without () are called method events, which also fall into two cases: for native events, the native event object is passed as the sole default parameter; for custom events, the parameters passed are any number of arguments from the second to the last (excluding the event name) when the event is emitted via $emit.

Talk is cheap, show me the code.

The above describes four scenarios:

  1. Native event with ()
1
2
3
4
5
6
<input type="text" @click="test1()">//模板里面传参为空
methods:{
test1(){
console.log(arguments);//则逻辑中的参数也为空;
}
}
  1. Custom event with ()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//子模板
<input type="text" @input="shuru($event)"/>
<div>{{xheldon}}</div>
//子逻辑:
name: 'child',
props:['xheldon'],
method:{
shuru(e){
this.$emit('haha', e.target.value,'其他参数');
}
}
//父模板
<child :xheldon="blob" @haha="something()"></child>
//父逻辑:
methods:{
something(){
console.log('parent:',...arguments);//上面 something() 中传 xxx, 则输出 'parent:xxx', 即忽略了子组件 $emit 事件时候传递的参数.
this.blob = arguments;
}
}
  1. Native event without ()
1
2
3
4
5
6
<input type="text" @click="test1">
methods:{
test1(){
console.log(arguments);//结果: 输出原生的 MouseEvent 事件
}
}
  1. Custom event without ()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//子模板
<input type="text" @input="shuru($event)"/>
<div>{{xheldon}}</div>
//子逻辑:
name: 'child',
props:['xheldon'],
method:{
shuru(e){
this.$emit('haha', e.target.value,'其他参数');
}
}
//父模板
<child :xheldon="blob" @haha="something"></child>
//父逻辑:
methods:{
something(){
console.log('parent:',...arguments);//input 输入 xxx, 则输出 'parent: xxx 其他参数', 即与子组件 $emit 时候传递的参数相关.
this.blob = arguments;
}
}

Directives and Parameters (Attributes)

Basic usage:

1
<div v-directive:propNamed.modifiers="value"></div>

Here, directive is called the directive, and propName is called the “parameter” of the directive. In practice, the parameter manifests as an attribute in html (Vue includes some built-in parameters/attributes, such as click in v-bind:click="method" or href in v-bind:href="/img/in-post/x.png". The former does not appear inline, while the latter, being required, appears as an inline attribute. Custom-defined parameters/attributes will always appear as inline attributes). The value is a variable (though enclosed in quotes) and in most cases comes from the parent template.

propName can include a modifier for quick operations like preventing default events (e.g., .prevent).

Some directives can be used directly, such as v-if, while others require parameters: v-bind:href or v-on:click.

Note that whether value is enclosed in quotes or not yields the same result—i.e., :propName="value" and :propName=value are equivalent. Unless otherwise specified, this applies to all cases below.

If value evaluates to false when converted to a boolean, propName is removed; if true, propName appears. Specifically, it follows these rules (only for custom attributes):

  1. For literals null, undefined, or false, the propName attribute is removed.
  2. If value is an undefined variable, such as :propName="wxd", propName is removed, and a warning is issued:
1
[Vue warn]: Property or method "s" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
  1. If value is an array (since arrays are objects), propName will always exist except in the second case below. The value behaves as follows:

    • :propName =[], :propName ="[]", :propName ="['']": value is removed, leaving only the attribute with no value.
    • :propName =[""]: The structure breaks.
    • :propName =["",""] or :propName ='["",""]': The value becomes ",".
  2. If value is a nested array, after flattening its values, if value or its recursive child elements contain a literal undefined or null (not written as a string), the value at that position is left empty; if value or its recursive child elements contain an Object, the value at that position becomes [object Object].

  3. If value is an object, propName is preserved, and the value becomes [object Object].

  4. If value is a number or string, such as :propName = "'fff'", propName is preserved, and value takes the string or numeric value.

After reviewing the source code, this is indeed the logic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function setAttr(el, key, value) {
if (isBooleanAttr(key)) {
// set attribute for blank value
// e.g. <option disabled>Select one</option>
if (isFalsyAttrValue(value)) {
el.removeAttribute(key);
} else {
el.setAttribute(key, key);
}
} else if (isEnumeratedAttr(key)) {
el.setAttribute(
key,
isFalsyAttrValue(value) || value === 'false' ? 'false' : 'true'
);
} else if (isXlink(key)) {
if (isFalsyAttrValue(value)) {
el.removeAttributeNS(xlinkNS, getXlinkProp(key));
} else {
el.setAttributeNS(xlinkNS, key, value);
}
} else {
if (isFalsyAttrValue(value)) {
el.removeAttribute(key);
} else {
el.setAttribute(key, value);
}
}
}

These rules only apply to custom properties. For built-in properties, the behavior differs. For example, when binding the class attribute:

1
v-bind:class="{active: isActive}"

This means if the value of isActive is false or any other value that can be converted to a boolean false, the active class name will not be applied. Otherwise, it will be applied (consistent with the truthiness evaluation of an if statement).

Filters

When filters are chained, the first parameter of the first filter is the initial value, while the first parameter of subsequent filters is the return value of the previous filter (or undefined if there is no return value).

Template:

1
2
3
<div v-bind:prop="rawProp | filterOne | filterTwo">
控制台查看 filter 函数的参数
</div>

Logic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default {
data() {
return {
rawProp: 'this is raw prop',
};
},
filters: {
filterOne() {
console.log(arguments);
return 'this param is pass to next filter';
},
filterTwo() {
console.log(arguments);
},
},
name: 'com-header',
};

Except for the first filter, subsequent filters cannot access the initial value. Of course, if you want to pass parameters, there are plenty of workarounds, such as having the first filter return an array, etc.

List Rendering

In v-for, if two parameters are provided, they align with the parameters of the native js forEach method: value, key. Using the of operator has the exact same effect as using the in operator—even though this isn’t the case in native js.

Another point to note is that when using v-for with components, the parent cannot automatically pass data to the component because the component has its own isolated scope. Therefore, to pass data to child components, you need to use the props property, which is slightly more verbose:

1
2
3
4
5
6
<my-component
v-for="(item, index) in items"
v-bind:item="item"
v-bind:index="index"
>
</my-component>

Additionally, when v-for is used on an object, the iteration yields the object’s values, not its keys. This differs from native js, where a for in loop requires manually accessing obj['i'] to output values, and a second parameter (value, key) is needed to output keys:

1
2
3
4
5
6
obj:{
first: 'xheldon',
last: 'cao',
age: '25'
}
<div v-for="(value, key) in obj">{% raw %}{{value}}-{{key}}{% endraw %}</div>// 输出 xheldon cao 25

Note that in native js, unless you manually implement a Symbol.iterator, you cannot use a for of loop. However, Vue allows it—though the effect is identical to for in.

There’s also a small tip in list rendering called the “in-place reuse” principle. What does this mean? Take the tololist example mentioned earlier. If no unique key is assigned to each element, like this:

1
2
3
4
5
6
7
<li v-for="(value, key) in todolist">
{%raw%}{{value.key}}:{{value.value}}{%endraw%}<button
@click="deletetodo(key)"
>
X
</button>
</li>

Then every time you click the “×” to delete the current li, Vue will reuse the existing elements in place, simply moving the data to the correct position instead of remove-ing the deleted dom element to avoid reflow. Below is the page render behavior when clicking “×” to delete an element, as shown by Chrome’s devtool:

没有key

You can see that the reflow portion is only the very bottom part.

When key is added:

1
2
3
4
5
6
7
<li v-for="(value, key) in todolist" :key="value.key">
{% raw %}{{value.key}}:{{value.value}}{% endraw%}<button
@click="deletetodo(key)"
>
X
</button>
</li>

Let’s look at the browser’s render behavior after clicking the close button:

有key

Some might wonder: Why do we need to manually implement a value.key on value here instead of using the key provided by Vue in (value, key)?:

1
2
3
4
5
{% raw %}
<li v-for="(value, key) in todolist" :key="key">
{{value.key}}:{{value.value}}<button @click="deletetodo(key)">X</button>
</li>
{% endraw %}

The answer is that while the key provided by Vue appears to be a key, it remains unrelated to the current data. Thus, when deleting an li, the key is merely recalculated without being removed or shifted along with the deleted or affected elements. If the above approach is used, the effect is the same as the first method without a key—it still employs the in-place reuse strategy, where the data changes while the dom structure remains unchanged. Therefore, you need to manually implement a key, roughly like this:

1
2
3
4
5
6
7
8
9
10
11
12
data(){
return{
toolist:[],
truekey: 0,//手动实现一个 key
todotext: ''
}
},
methods: {
addTolist(){
this.todolist.push({value:this.todotext, key: ++this.truekey});// 把 key 放到 data 上
}
}

Event Handlers

Event handlers can be chained, but some elements inherently don’t support certain events, making binding meaningless—for example, binding a keyup event on a div:

1
<div @keyup.alt="pressalt">div alt 按键测试</div>

Thus, it’s common to handle events via bubbling on a div and then bind an alt+ctrl event on an input:

1
2
3
<div @keyup.alt="presskey">div冒泡按键测试
<input type="text">
</div>

I have a concern here: If an input uses @keyup.space for listening, but in Chinese input methods, the spacebar is typically used to select words, will the actual output be the unselected pinyin letters or the first candidate word after pressing the spacebar? (This might not be a Vue issue, but I’ll mention it here.)

The answer is that in most cases, the result is the first candidate word after pressing the spacebar. However, if a sentence is very long and requires pressing the spacebar twice to output the entire phrase, the first press will output nothing (blank), and the second press will output the full phrase. I tested this edge case, so it’s safe to assume the output will be the first candidate word after pressing the spacebar, not blank or pinyin letters. I used Sogou mac input method’s single-line candidate mode for testing (details omitted).

Note: The official documentation mentions IME when discussing v-model, referring to this issue. If you want v-model to respond immediately during IME composition, you can bind the input event.

Form Controls

v-model is typically used on input elements, and the value attribute in the template will be ignored—v-model only recognizes the initial value in js and binds to it. So if you write both v-model and a value attribute, the latter will appear in the dom structure but will be ignored when js retrieves the value, as v-model takes precedence:

1
2
3
4
<input
type="text"
value="我出现在 dom 结构的 value 属性中, 但是只能通过 getAttribute 获取到我, .value 并不能获取到, 伤心~"
/>
1
2
3
4
5
data(){
return{
todotext:'我是真正的初始值, js 获取的是我, 虽然我并不出现在 dom 中的 value 属性中~',
}
}

The difference between the two is similar to that between .data and .attr in jQuery—what’s written inline is the value of attr('data','xxx'), and inspecting the dom structure also shows the value xxx. However, the actual value obtained via js is the one bound through .data('yyy')—unless you use attr to read the dom structure. (Note: If you modify the attr value after instantiation, js will retrieve the attr value. The initial value is ignored here—only the initial value. For example, after initialization, if v-model binds to value and you manually modify the value in the dom structure, v-model will then use the modified attr value instead of the value on data.)

If the requirement is unconventional—say, you don’t want to use v-model to get and update the input value in real-time, or you need to process the input value before updating it—you can try $ref (e.target.value also works):

Template:

1
<input @input="input" ref="inputvalue" />

Logic:

1
2
3
4
5
6
7
8
data(){
message: ''
},
methods:{
input(){
this.message = this.$refs.inputvalue.value;
}
}

The official documentation also states that v-model is just syntactic sugar for two-way data binding:

1
<input v-bind:value="something" v-on:input="something = $event.target.value" />

However, listening for the input event has a drawback: when using an input method, the event triggers even before pressing space to select a word. So, unless this behavior is needed, it’s better to stick with v-model.

For binding multiple elements to the same value and outputting them—a common requirement for a group of checkboxes—you need to use an array:

1
2
3
<input v-model="messages" type="checkbox" value="xheldon" />
<input v-model="messages" type="checkbox" value="xiaodan" />
<p>{{messages}}</p>
1
2
3
4
5
data(){
return{
messages: []
}
}

(As far as I’ve discovered,) this concise array usage only works for multiple checkbox-type inputs—i.e., several elements bound to the same v-model, where their states aren’t synchronized but their corresponding values are collected into an array. Of course, if you insist on using methods to achieve similar effects for arbitrary input types—well, never mind.

For a single checkbox, v-model binds to the value, which is either true or false. You can customize the selected and unselected values using :true-value and :false-value.

For multiple radio buttons, v-model serves a role similar to name—i.e., grouping them. Thus, radio-type inputs using v-model don’t need the name attribute.

For select types, if no value attribute is given for each option, the bound value is the content of the option; otherwise, it’s the value attribute. For multi-select selects, the data bound to v-model must be an array; otherwise, a warning is issued (though it won’t error—Vue auto-converts it, and it still runs):

1
Vue warn]: <select multiple v-model="selectM"> expects an Array value for its binding, but got String

Note: For all the above types, when v-model binds to value and the value attribute is dynamically bound (:value="xxx") to another property (xxx) on data, the properties corresponding to v-model and :value are the same (strictly equal).

Components

First, we need to distinguish between what is a DOM template and what is a string template.

An HTML template refers to ordinary html elements that are bound through the el option of a Vue instance:
String template section:

1
2
3
4
5
6
7
8
9
10
<div id='tpl'>
{' '}
实例挂载元素 html 自有组件:
<ul>
<li></li>
<li></li>
</ul>
自定义组件:
<my-component></my-component>
</div>

A string template refers to:

  1. Templates registered via template in js, such as:
1
2
3
4
5
6
Vue.component('my-component', {
template: '<span>{{message}}</span>',
data() {
message: 'hello, xheldon';
},
});

Or:

1
2
3
4
5
6
7
8
var Child = {
template: '<div>'hello, xheldon'</div>'
}
new Vue({
components: {
'my-component': Child
}
})
  1. Templates registered via <script type="text/x-tempalge"></script> (similar to Handlebar).

  2. The content inside the <template> tag in .vue components.

Because Vue parses DOM templates only after the browser has finished parsing, DOM templates cannot use components in certain tags that require specific child elements. For example, the child elements of a select tag must be option, so a custom tag like com-option won’t be recognized. To address this, you can add an is="component-name" attribute to indicate the template name used by the tag.

Literal Syntax vs. Dynamic Syntax

Here’s an important note: In native js, object properties can be numbers, though they are treated as strings (limited to ES5; in ES6, object properties can be any value, but this doesn’t conflict with what follows). However, in Vue, the data property cannot use numbers as keys. If literal syntax is used, passing a number will first be converted to a string via toString. If dynamic syntax is used, it will be treated directly as a number and won’t look for the corresponding property value bound in data:

Child component template:

1
2
<div>{%raw%}{{dynamic}}{%endraw%}</div>
<button @click="alertProp">点我看上面 props 传参的类型</button>

Child component logic:

1
2
3
4
5
6
props:['dynamic'],
methods:{
alertProp(){
alert(typeof this.dynamic)//上面说过了, props 属性也是绑定到 Vue 实例上的, 因此可以直接使用 this
}
}

Parent component—literal syntax:

1
<com-child dynamic="11"></com-todolist>

Parent component—dynamic syntax:

1
<com-child :dynamic="11"></com-todolist>

In both cases, the parent component logic is:

1
2
3
4
5
data(){
return {
11:'属性-动态语法'
}
}

The result is: When using the parent component’s literal syntax, clicking the button passes 1 to the child component, and as the documentation states, it’s the string "1", so alert shows string. When using the parent component’s dynamic syntax with v-bind bound to the 1 property in the parent’s data, the child component doesn’t receive the value corresponding to the 1 property (i.e., "属性-动态语法"), but rather the number 1 itself. Thus, clicking the button triggers an alert showing number.

Conclusion: It’s best not to use numbers as properties in data objects.

Note: If the property passed to the child component is an array or object, modifying this property in the child component will reflect in the parent component—this is usually undesirable. As the saying goes: props down, events up (example omitted). The best practice is to use a deep copy of reference types passed from the parent—unless you explicitly need the child component to affect the parent’s state, in which case, good luck.

When events up, if the child component passes additional parameters besides the event name in $emit, these parameters will be passed to the parent component’s event listener function:

Child component template:

1
2
<input type="text" @input="someEventHandlerForOriginalEventLikeClickOrInput($event)"/>
<span>{{data}}</span>

Child component logic:

1
2
3
4
5
6
props:['data'],
methods:{
someEventHandlerForOriginalEventLikeClickOrInput(e){
this.$emit('eventPasstoParent', e.target.value, 'someOtherArgus');
}
}

Parent template:

1
2
3
4
<com-todolist
:data="someParentData"
@eventPasstoParent="someParentEventHandler"
></com-todolist>

Parent logic:

1
2
3
4
5
6
someParentData: ''
methods:{
someParentEventHandler(){
this.someParentData = [...arguments];
}
}

Asynchronous Update Queue

With Vue, jQuery is no longer necessary. The greatest advantage of frameworks is avoiding repetitive code for the same operations. Two-way data binding can solve many DOM manipulation issues, but in some cases, jQuery still has its advantages. For example, when manipulating the DOM, jQuery accepts a function as a callback, which triggers after the animation completes. With our two-way data binding, after setting the data, how do we know the DOM has been updated? The answer is the same as with jQuery: the asynchronous update queue.

1
2
3
4
5
6
7
8
9
10
11
var vm = new Vue({
el: '#example',
data: {
message: '123',
},
});
vm.message = 'new message'; // 更改数据
vm.$el.textContent === 'new message'; // false
Vue.nextTick(function () {
vm.$el.textContent === 'new message'; // true
});

I personally don’t recommend writing it this way because I believe the best logic should be placed within the instance’s properties/methods rather than outside it. Fortunately, Vue provides a way to achieve this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vue.component('example', {
template: '<span>{{ message }}</span>',
data: function () {
return {
message: 'not updated',
};
},
methods: {
updateMessage: function () {
this.message = 'updated';
console.log(this.$el.textContent); // => '没有更新'
this.$nextTick(function () {
console.log(this.$el.textContent); // => '更新完成'
});
},
},
});

Animation

There’s not much to say about animations, but two hooks in the JavaScript hook functions need mentioning: enterCanceled and leaveCancelled. enterCancelled is used with v-if and v-show and can be triggered in both cases. The timing of its trigger is after the enter event is fired but before the animation completes, when another animation needs to be executed. On the other hand, leaveCancelled is only used with v-show and is ineffective with v-if—it will never be triggered. Its trigger timing is when the leave animation (i.e., the xxx-leave-active animation) is interrupted by another animation before completion.

Test code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<button type="button" @click="show=!show">点击我切换状态</button>
<transition
name="go"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
>
此处使用 v-show, 修改成 v-if 的时候发现, leave-cancelled 不会触发.
<p v-show="show">点击展示我, 再点击一下隐藏我.</p>
</transition>

Logic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data(){
return{
show: true
}
},
methods: {
beforeEnter(){alert(1);},
enter(){ alert(2);},
afterEnter(){ alert(3);},
enterCancelled(){ alert(4);},
beforeLeave(){ alert(5);},
leave(){alert(6);},
afterLeave(){ alert(7);},
leaveCancelled(){ alert(8);}
},

Styles:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.button-animate button {
margin-left: 20px;
margin-top: 40px;
transition: all 1s ease;
position: absolute;
}
.go-enter-active {
transition: all 5s ease;
}
.go-leave-active {
transition: all 5s cubic-bezier(1, 0.5, 0.8, 1);
}
.go-enter,
.go-leave-active {
transform: translateX(10px);
opacity: 0;
}

Apart from the enter and leave hooks, which take the el element itself (a native Element type) and the done callback function as parameters, all other hooks only take the el element as their parameter.

For element transitions, it’s best to add a key to each element. Due to the in-place reuse strategy mentioned earlier, data might be replaced directly during transitions without any animation effects.

One point not mentioned at all in the official documentation is that the order of CSS class names for Vue transition animations is restricted. v-enter and v-leave must be written after v-enter-active and v-leave-active; otherwise, they won’t work. For example, if I want to create a fade-in/fade-out effect for a button click—where clicking the button triggers a new button fading in from left to right while the currently clicked button fades out from left to right—the logic would be:

1
2
3
4
5
6
7
8
data:{
isShow: true
},
methods:{
animakkey(){
this.isShow = !this.isShow;
}
}

Structure:

1
2
3
4
5
6
<div class="button-animate">
<transition name="go">
<button key="a" v-if="isShow" @click="animakkey">on</button>
<button key="b" v-else @click="animakkey">off</button>
</transition>
</div>

If your styles are written like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.button-animate button {
margin-left: 20px;
margin-top: 40px;
transition: all 1s ease;
position: absolute;
}
.go-enter {
opacity: 0;
transform: translateX(-25px);
}
.go-leave {
opacity: 1;
transform: translateX(25px);
}
.go-enter-active {
opacity: 1;
transform: translateX(0px);
}

.go-leave-active {
opacity: 0;
transform: translateX(50px);
}

You’ll find the animation effect unsatisfactory:

animate

But if you place enter after enter-active:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.button-animate button {
margin-left: 20px;
margin-top: 40px;
transition: all 1s ease;
position: absolute;
}
.go-enter-active {
opacity: 1;
transform: translateX(0px);
}
.go-enter {
opacity: 0;
transform: translateX(-25px);
}
.go-leave-active {
opacity: 0;
transform: translateX(50px);
}
.go-leave {
opacity: 1;
transform: translateX(25px);
}

It works perfectly:

animate

After comparing all possible orders of these four CSS class names, I found that as long as v-enter is placed after v-enter-active, the effect works. The order of the other class names doesn’t matter.

The transition tag should only contain the elements to be animated and no other elements. If the structure in the example above is written like this:

1
2
3
4
5
6
<transition name="go">
<div class="button-animate">
<button key="a" v-if="isShow" @click="animakkey">on</button>
<button key="b" v-else @click="animakkey">off</button>
</div>
</transition>

No animation effect will occur. Additionally, if some CSS properties defining the element’s style are set in normal CSS class names, and the same properties are used in animation class names like v-enter, they won’t take effect. The documentation claims “their priority is higher than normal class names,” but this isn’t actually the case (or perhaps I misunderstood—corrections are welcome). Using the same example, if you set the normal CSS properties for button in the styles:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.button-animate button {
margin-left: 20px;
margin-top: 40px;
transition: all 1s ease;
position: absolute;
transform: translateX(50px);
}
.go-enter-active {
opacity: 1;
transform: translateX(0px);
}
.go-enter {
opacity: 0;
transform: translateX(-25px);
}
.go-leave-active {
opacity: 0;
transform: translateX(50px);
}
.go-leave {
opacity: 1;
transform: translateX(25px);
}

The result is:

animate

As you can see, only opacity produces an animation, while transform does not! (Students can test whether the animation effect still occurs when using animate.css by pre-setting elements with the same properties as in Animate.css. Feel free to submit an issue if you encounter any problems.)

transition-group is slightly different from transition. Visually, transition itself is just a wrapper container and does not participate in the page structure, whereas transition-group is replaced by Vue with a tag—defaulting to a span tag, though the tag name can be customized.

The render Function

The render function can replace the role of writing templates. Its parameter, createElement, is typically abbreviated as h. Various bindings/attributes within components or tags can all be expressed using corresponding JavaScript syntax in createElement. If something isn’t found, it means you can use native methods, such as .stop or .prevent, which can be directly replaced with event.stopPropagation() and event.preventDefault().

Other Topics

Mixin refers to modifying or adding extra functionality during the normal component writing process (i.e., within the component’s lifecycle).

Some plugins are built based on the above mixin concept. Beyond that, plugins may also add methods to Vue.prototype or introduce global methods or properties via config.

Routing can be simply implemented using the is attribute of a component, or by writing a render function to render different templates based on different paths. For more complex scenarios, third-party libraries are needed.

State Management: The example suggests adding a wrap to record each state change process. The official best practice recommends modifying state through functions rather than directly assigning values to instance properties, as this makes state changes traceable.

Unit Testing is standard unit testing—nothing special to mention.

Server-Side Rendering (Server Side Render)

The basic idea is straightforward: First, exports a Vue instance in app.js. Then, create a page template file index.html (which includes Vue.js and the $mount method for mounting the Vue instance—whether app.js also needs to be included, as shown in the official example, requires further verification). The template should contain a mount point (a non-empty element with an id attribute). In the server-side server.js, import these files and use vue-server-renderer to render the Vue instance exported from app.js. Finally, replace the mount point (a non-empty element with an id attribute, since the template from app.js already exists) when returning the content to the client.

The result of server-side rendering is that the mount point (a non-empty element with an id attribute) will have a server-rendered="true" attribute added (visible by right-clicking to view the page source, confirming it is not dynamically added by js).

The server also supports streaming rendering. First, the html needs to be split at the mount point (a non-empty element with an id attribute, such as <div id="app"></div>), dividing it into two parts, a and b. Previously, vue-server-renderer used renderToString for app.js, but to support streaming rendering, we need to switch to a method called renderToStream. Then, listen for the data event and append it after part a of the html. After the end event, concatenate part b of the html, and finally send it all out via res.send.

Postscript

Located in the capital, ping cn.vuejs.org:

ping

ping my company’s FQ VPS:

ping

dig it:

dig

You can see it uses cloudflare services. International websites are tough to maintain, huh.

- EOF -
Originally published at: Vue Learning Summary - Xheldon Blog