天,请注意时效性
前言
这篇文章从 Vue 2.0官方文档
的"实例"一节开始, 研究一些 Vue API
的使用方法, 以及 Vue
实现一些功能的原理, 此外还有自己的使用感受, 以及站在自己浅薄的角度分析 Vue
为什么要这么设计的不敬之举, 如有得罪还请海涵, 刚接触 Vue
不久, 不当之处烦请指出, 先行谢过.
注意: 基础知识直接略过, 我只说我认为需要说的点.
Vue 实例
每个 Vue.js
的应用都是通过构造函数创建一个 Vue
的根实例启动的, 意思就是, 每个页面的数据都应该只由这一个实例维护, 原始数据的来源都应该只由根实例来发出和接收统一管理, 根实例再通过 props
, 分发数据, 或者 events
来监听数据. 子组件只需要 watch/computed
数据变化, 及时更新即可.
文档中说了一句话叫: 所有的 Vue.js 组件其实都是被扩展的 Vue 实例
, 这句话正确理解起来应该是, 你可以在组件上使用和实例一样的方法和钩子函数, 除了 data
.
组件中的 data
, 必须是一个函数, 因为组件会被复用, 所以必须每次调用组件都生成一份数据.
数据代理(proxy), 指的是 Vue
实例会代理其 data
对象中的所有属性, 而实例属性 $data
则表示 data
属性本身, 以区别被代理的 data
.
意思是, 如果一个 vm
的 data
属性为 {a: 'xheldon'}
, 那么 vm.a
即为 'xheldon'
, 而 vm.$data
则为 {a: 'xheldon'}
.
组件其实也是一个(被扩展的) Vue
实例, 下面是个简单的验证:
有一个 list.vue
组件(template
和 style
省略):
1 |
|
props vs data
初始化组件的时候, prop
上的属性和 data
上的属性以及 computed
的方法, 都被绑定到 Vue
实例上了, 但是 porps
上的属性, 优先级比 data
同名属性要高,下面是验证:
1 |
|
结果出现警告(不是报错, 不影响渲染):
1 |
|
而 computed
返回的函数名和 data
上的属性名可以重复, 并且不会有任何提示, 但是同名覆盖了之后, 因为在初始化的时候, 从控制台可以看到 _data
后于 _computedWatcher
来设置这个重复属性的 getter
和 setter
(不知道是不是这个原因, 先放在这个地方以待我深入研究之后再修改这篇文章), 因此导致了被覆盖. 相关原理可以 看这篇介绍
它们都在初始化的时候绑定到了实例 属性
上, 同名的 computed
属性被覆盖了, 但是 Vue devtool
仍然正确显示了出来)
1 |
|
Vue devtool
正确显示了出来:
如果 computed
上的方法名和 data
上的属性名不重复:
1 |
|
此外, methods
和 computed
方法的区别除了后者有缓存而前者没有外(即后者除非其所依赖的响应式数据发生变化, 否则不会重新计算), 如果两者均是为了返回插值的话,则 methods
上的方法使用是 functionName()
,
而 computed
上的方法引用是 functionName
, 即前者需要执行函数, 后者不需要执行.
究其原因, 是因为 computed
属性上的我们写的方法, 被当做一个属性, 挂载到 Vue
实例上了, 而我们提供的函数, 被当做此方法的 getter
.
而 methods
不同, 它就是一个函数, 因此无论是插值引用, 还是事件方法, 都需要使用()来调用.
这样的话, 当需要真正的函数------这里如果使用函数调用, 则 computed
需要返回一个函数不仅仅指是返回一个值, 而是需要传递参数------的时候, metohds
首当其冲.
下面是一个不同于官网的另一个版本的 todo list
:
模板:
1 |
|
逻辑:
1 |
|
此例子中, 如果使用这种模板, 则需要传递一个参数, 以在点击 button
的时候删除当前 li
, 因此这个情况必须使用 methods
.
使用 computed
的时候, 不接受参数 (因为是一个 getter
), 即使其返回一个 function
:
1 |
|
注意: 若两者存在同名函数, 那么 computed
上的函数优先级比 methods
高(处理 getter
和 setter
先后顺序的问题, 具体可查看源码), 这个情况无论是对插值还是事件绑定都适用, 以下是验证:
插值引用测试:
模板:
1 |
|
1 |
|
输出函数 im from computed
, 如果 computed
不返回函数, 插值引用只使用 {{a}}
则显而易见更是输出 computed
的值, 验证此处略.
事件绑定的时候:
调用的时候使用 内联处理器方法:
1 |
|
逻辑:
1 |
|
输出 computed
如果使用 方法事件处理器, 结果一样:
1 |
|
逻辑:
1 |
|
输出 computed
注意事件绑定的时候, computed
都需要返回一个函数.
因此优先级的顺序是:
porps > data > computed > methods
我猜测在 computed
和 methods
中的 this.xxx
中的引用优先级也是相同的, 想搞明白的可以去验证下.
而且可以看到, 两种情况 method
都不用返回一个 function
, 而两种情况 computed
都需要返回一个 function
而且都不接受参数(因为是 getter
). 综上所述, 事件处理最好使用 methods
, 而数据绑定/插值处理 最好使用 computed
(因为有缓存).
此外, 对于 method
绑定事件的时候, 带()和不带()的效果是一样的, 都会执行其函数, 区别有以下几个:
带()的叫内联语句
, 分两种情况, 原生事件和自定义事件. 两种情况都可以传参, 如果参数列表为空, 则默认参数arguments
也为空, 即不存在默认参数; 如果是被一个原生事件如input/click
触发的, 则可以传递一个特殊的$event
参数作为原生event
事件处理; 而如果是被一个自定义事件触发的, 其事件处理函数的参数仍然取决于实际传递给事件处理函数的值, 而且自定义函数不存在特殊的$event
对象供使用. 由$emit
触发自定义事件时传递的参数将被忽略.不带()叫方法事件
, 则分两种情况: 如果是原生事件, 则会传递原生event
事件作为唯一的默认参数; 如果是自定义事件, 则传递的是$emit
事件的时候除了事件名外的第二到最后一个任意数量的参数.
talk is cheap, show me the code
上面说的是四个情况:
1.带()的原生事件
1 |
|
2.带()的自定义事件
1 |
|
3.不带()的原生事件
1 |
|
4.不带()的自定义事件
1 |
|
指令和参数(属性)
基本用法:
1 |
|
其中, directive
叫做指令, propName
叫做指令的"参数", 实际上参数的具体表现就是 html
上的属性(Vue
内置了一些参数/属性, 如 v-bind:click="method"
, 的 click
, 如 v-bind:href="/img/in-post/x.png"
的 href
, 前者不会出现在行内, 而后者因为是必须的因此会出现在行内属性. 而自己定义的参数/属性, 一定会出现在行内属性). 而 value
是个变量(虽然它写在双引号中)大多数情况下来自于父级模板.
propName
可以带个修饰符, 用来快捷操作一些行为如禁止默认事件 .prevent
等.
有些指令可以直接使用如 v-if
, 有些必须加上参数: v-bind:href/v-on:click
注意, value
带不带双引号结果都是一样的, 即 :propName="value"
和 :propName=value
是一样的, 除特殊说明, 下列所有情况均适用.
如果 value
转换成布尔值后为 false
则 propName
被移除, 为 true
则该 propName
出现, 实际上, 它遵守以下规则(仅限自定义属性):
1.null
, undefined
, false
的直接量, propName
属性被移除. 2.value
为一个未定义的变量, 如 :propName="wxd"
则 propName
移除, 且出现警告:
1 |
|
3.如果 value
为一个数组, 因为数组为对象, 因此 propName
除了下面第二种情况外, 恒存在, 而 value
分以下情况:
-
:propName =[]
,:propName ="[]"
,:propName ="['']"
value
移除, 即只有属性没有值. -
:propName =[""]
, 结构乱掉. -
:propName =["",""]
或者:propName ='["",""]'
,value
值为","
-
如果
value
为一个嵌套数组, 则其值被一维化后, 如果value
包含及其递归子元素包含一个为undefined
或者null
直接量(不是写到字符串里面的), 则该处的值留空; 如果value
包含及其递归子元素包含一个Object
, 则该处的值为[object Object]
4.如果 value
为一个对象, 则 propName
保留, 值为 [object Object]
5.如果 value
为一个数字或者字符串, 如 :propName = "'fff'"
, 则 propName
保留, value
为字符串或数字值.
看了下源码, 也确实是这么个逻辑:
1 |
|
这些规则仅限自定义属性, 如果是内置属性, 则又不同, 比如绑定 class
属性:
1 |
|
则表示如果 isActive
值为是 false
或者是其他可以转换成布尔值 false
的值, 则 active
这个类名不应用, 反之, 则应用该类名(同 if 语句的真假判定一致).
过滤器
过滤器串联起来的话, 第一个过滤器的参数是初始值, 随后的过滤器第一个参数为上一个过滤器的返回值, 没有返回值则为 undefined
.
模板:
1 |
|
逻辑:
1 |
|
除了第一个 filter
,后面的 filter
没有办法获取到初始值. 当然, 如果你想传参数, 办法有的是, 比如第一个 filter
返回一个数组等等.
列表渲染
v-for
中, 如果参数是两个, 则是和原生 js
中的 forEach
参数一致, 是 value, key
, 而使用 of
操作符和使用 in
操作符的效果完全一样------虽然在原生 js
中并不是.
还有个需要注意的地方是, 组件使用 v-for
的时候, 父级是不能自动传递数据到组件的, 因为组件有自己的独立作用域. 因此你为了传递数据给子组件用, 需要使用 props
属性写的稍微麻烦一点:
1 |
|
另外, v-for
使用在对象上面的时候, 迭代值是对象的值, 而不是键, 这个和原生 js
不一样, 原生 js
的 for in
循环若要输出值, 需要你手动遍历 obj['i']
, 而想要输出键需要写第二个参数(value, key)
:
1 |
|
注意, 原生 js
除非你手动实现了一个 Symbol.iterator
, 否则是不能使用 for of
循环的, 但是 Vue
可以------虽然效果和 for in
完全一样.
列表渲染还有一个小 tips
叫就地复用原则, 什么意思呢? 还是拿上面说的 tololist
说, 如果没有给每个元素指定一个独一无二的 key
值, 就像这样:
1 |
|
那么每次点击这个叉叉删除当前 li
的时候, Vue
都会就地复用当前的元素, 直接移动数据到正确的位置, 而不是 remove
删除的 dom
元素, 避免 reflow
, 下面是使用 chrome
的 devtool
工具显示的当点击叉叉删除元素的时候, 页面 render
的情况:
可以看到 reflow
的部分只有最下面那一点
而当加上 key
之后:
1 |
|
再看点击叉叉之后浏览器的 render
的情况:
有人可能会有疑惑, 为什么这个地方需要自己手动实现一个 value
上的 value.key
, 而不是使用 Vue
给的 (value, key)
中的 key
呢?:
1 |
|
答案是, Vue
给的 key
, 看起来是个 key
, 但是还是跟当前数据是无关的, 因此当删除一个 li
的时候, key
仅仅只是重新算了一下, 并没有跟着删除的或者被删除的元素移除或者上移下移. 如果使用上面的写法, 效果和第一种没有 key
的是一样的, 仍然使用了就地复用策略, 变动的还是数据, 不变的还是 dom
结构, 因此你需要手动实现一个 key
, 大致是这样的:
1 |
|
事件处理器
事件处理器可以串联, 但是有些元素本身不支持, 因此绑定了也没有意义, 如在 div
上绑定一个 keyup
事件:
1 |
|
因此一般是在 div
上冒泡处理事件, 然后在一个 input
上绑定一个 alt+ctrl
事件:
1 |
|
我对此有个担心, 就是如果一个 input
使用了 @keyup.space
的监听, 但是中文输入法中空格一般是用来选中词语的, 那实际输出的是还未选中词语的拼音字母, 还是按下空格后, 候选列表的第一个词语呢?(貌似这个不是 Vue
的问题, 但是还是在这儿提出来了)
答案是, 大多数情况下, 结果是按下空格后的第一个词语, 但是如果一句话很长的话, 需要按两次 space
来输出一句话, 则第一次按的时候, 什么也不会输出, 是个空的, 第二次按下 space
才输出全部的词汇. 我这里是为了测试的极端情况, 因此基本可以认为按下空格后的第一个词语, 而不是空白, 或者拼音字母. 我使用的是搜狗 mac
输入法的单行候选词模式, 可以用上面的 todolist
来测试(略)
注: 官网文档讲 v-model
的时候会讲到 IME
, 说的就是这个问题, 如果希望使用输入法的时候, v-model
也即时响应, 那么可以绑定 input
事件.
表单控件
v-model
一般用在 input
上面, 而模板中的 input
的 value
值会被忽略------ v-model
只认在 js
中的初始值, 并与之绑定, 因此如果你写了 v-model
, 又写了 value
属性, 则后者虽然会出现在 dom
结构中, 但是 js
获取其值的时候会忽略掉它而取 v-model
绑定的值:
1 |
|
1 |
|
二者的区别类似于 jQuery
中的, .data
和 .attr
的区别------写在行内的是 attr('data','xxx')
的值, 查看 dom
结构看到的也是 xxx
的值, 但是 js
获取到的实际的值是通过 js
绑定的 .data('yyy')
的值------当然除非你使用 attr
读取 dom
结构.(注意, 实例化之后再修改 attr
的值, js
获取到的就是 attr
的值了, 这里的忽略初始值, 仅仅是忽略初始值而已, 举个例子就是初始化之后的 v-model
绑定了 value
之后, 手动修改 dom
结构的 value
值, 那 v-model
再获取该元素的 value
值就会使用修改后的 attr
属性值, 而不是 data
上面的值).
如果需求比较奇葩, 不想通过 v-model
获取 input
的值, 然后实时更新, 或者需要获取 input
的值处理后再更新, 同时不想使用 v-model
, 可以试试 $ref
(e.target.value
也是可以的):
模板:
1 |
|
逻辑:
1 |
|
官网文档也说了, v-model
只是一个实现双向数据绑定的语法糖:
1 |
|
不过监听 input
事件, 导致的问题是使用输入法的时候, 在没按空格选中词语的时候, 也会触发输入的事件, 所以如果没有这个需求, 还是老老实实用 v-model
的好.
如果需要多个元素绑定相同的值并输出, 常见的需求是一组 checkbox
, 这个时候需要使用数组:
1 |
|
1 |
|
(目前我发现的)这种如此简洁的数组用法仅仅对多个 checkbox
类型的 input
有效------即几个元素绑定相同的 v-model
, 但是这几个元素的状态却没有同步, 而是将对应的 value
放到数组中. 当然如果你强行说, 我用 methods
方法可以实现任意输入类型的元素完成类似效果------那当我没说.
单个的 checkbox
v-model
绑定的是 value
值, 为 true
或者 false
, 可以通过 :true-value
和 :false-value
来自定义选中时候的值和没有选中时候的值.
而多个单选框 radio
中的 v-model
起到了类似 name
的作用------即用来分组, 所以 radio
类型的 input
使用 v-model
的话就不用写 name
属性.
select
类型的如果没有给定每个 option
的 value
属性, 则绑定的是 option
中的值, 如果给了则就是 value
的属性值. select
类型的多选框 v-model
绑定的 data
必须是一个数组类型, 否则会给出警告(但不会报错, Vue
自动转换, 还是能正常运行):
1 |
|
注意, 以上所有类型的 v-model
和 value
绑定时, 而 value
属性又动态 (:value="xxx"
)绑定了 data
上的其他(xxx
)属性, 则 v-model
对应的属性和 :value
对应的属性是同一个(严格相等).
组件
首先需要区分的是, 什么是 DOM
模板, 什么是字符串模板.
HTML
模板指的是普通的 html
中的元素, 这些元素会通过 Vue
实例的 el
选项进行绑定:
字符串模板部分:
1 |
|
字符串模板指的是:
1.js
中通过 template
注册的模板如:
1 |
|
或者:
1 |
|
2.通过<script type="text/x-tempalge"></script>
注册的模板(和 Handlebar
一样)
3..vue
组件中的 <template>
标签内的内容.
因为 Vue
是在浏览器解析完毕之后才开始解析 DOM
模板的, 因此 DOM
模板在一些需要特定子元素的标签上不能使用组件. 如 select
标签下的子元素必须为 option
, 因此用自定义标签 com-option
则不会识别, 因此可以增加一个 is="component-name"
属性, 表明该标签使用的模板名字即可.
字面量语法 VS 动态语法
这里需要注意个问题, 即在原生 js
中, 对象的属性是可以为数字的, 只是其会被当成是字符串(仅限 ES5
, ES6
中对象属性可以为任意值, 不过和之后要说的不冲突). 但是在 Vue
中, data
属性上不能使用数字作为属性, 如果是字面量语法, 传递数字会被先 toString
处理, 而如果是动态语法, 则会直接当做是数字处理, 不会寻找 data
上绑定的属性值:
子组件模板:
1 |
|
子组件逻辑:
1 |
|
父组件-字面量语法:
1 |
|
父组件-动态语法:
1 |
|
以上两种语法下父组件逻辑均为:
1 |
|
结果是, 当使用父组件字面量语法毫无疑问点击 button
的时候传递给子组件的是 1
, 而且文档也说了, 是字符串的 1
, 因此 alert
出来的是 string
; 而当父组件动态语法使用 v-bind
绑定了父组件 data
的 1
属性, 但是子组件并没有接收到 1
属性对应的 属性-动态语法
这个值, 而还是 1
这个值, 因此点击 button
时候 alert
出来的是 number
.
结论: 最好不要使用数字作为 data
对象的属性.
注意: 如果传递给子组件属性的是一个数组或者对象, 在子组件中修改这个属性值, 则会反映到父组件上------这通常是不应该的, 因为俗话说得好: props down, events up
(举例略), 最佳实践应该是使用父组件传递过来的引用类型的深拷贝------当然如果你就是需要子组件影响父组件的状态, 那我祝你好运.
events up
的时候, 如果子组件 $emit
的时候传递了除了事件名之外的其他参数, 则这些参数会被传递给父组件的事件监听函数:
子组件模板:
1 |
|
子组件逻辑:
1 |
|
父级模板:
1 |
|
父级逻辑:
1 |
|
异步更新队列
有了 Vue
, 就不再需要 jQuery
了, 框架的最大好处是避免了我们对相同操作的写出重复的代码. 双向数据绑定可以帮我们解决很多 DOM
操作问题, 但是有些情况下 jQuery
却更有优势, 比如操作 DOM
的时候, jQuery
会接收一个函数作为回调函数, 动画执行完成的时候触发. 而我们使用的双向数据绑定, 设置完数据之后, 如何知道 DOM
已经更新了呢? 答案和 jQuery
一样, 就是异步更新队列.
1 |
|
这么写我个人是不推荐的, 因为我认为最好的逻辑是写到实例的属性/方法里面, 而不是写到实例的外面, 还好 Vue
给我们提供了实现方式:
1 |
|
动画
动画没什么好说的, 主要是 JavaScript
钩子函数中的两个钩子需要说下, 一个是 enterCanceled
和 leaveCancelled
. enterCancelled
在 v-if
和 v-show
中使用, 均可能触发, 而触发时机, 是触发 enter
的事件之后, 在动画还没有执行完的过程中, 又需要执行其他动画的时候. 而 leaveCancelled
只用于 v-show
中, 在 v-if
中使用时无效的, 永远不会被触发, 其触发时机是在离开动画(即 xxx-leave-active
动画)播放未完成的时候, 又执行了其他动画的时候触发.
测试代码:
1 |
|
逻辑:
1 |
|
样式:
1 |
|
钩子们的参数除了 enter
和 leave
是 el
元素本身(原生的 Element
类型元素), 和 done
回调函数外, 其他钩子的参数均为 el
元素本身.
元素的过渡, 最好给每个元素加个 key
, 因为之前提到的就地复用策略, 可能在切换的时候直接替换数据, 没有动画效果.
在官方文档中完全没有说明的一点是, Vue transition
的动画类名 css
的写法是有顺序限制的, v-enter
和 v-leave
必须写在 v-enter-active
和 v-leave-active
的后面, 否则无效, 比如我想写一个点击按钮的淡入淡出效果, 点击按钮之后会有一个按钮从左向右淡入, 同时当前点击的按钮任从左往右淡出:
逻辑:
1 |
|
结构:
1 |
|
如果你的样式是这样的:
1 |
|
会发现动画效果并不如意:
而如果把 enter
放到 enter-active
的后面:
1 |
|
就完美了:
在对比了这四个 css
类名的可能顺序之后, 发现只要 v-enter
放到 v-enter-active
的后面就能实现效果, 其他类名随意.
transition
标签中不能放其他元素而只能是放需要动画的元素, 如果上述示例结构写成这个样子:
1 |
|
则不会有任何动画效果, 而如果在正常 css
类名中使用一些 css
属性规定了元素的样式, 而在动画类名如 v-enter
中又使用了相同的属性, 则也不会生效, 文档 说"他们的优先级高于普通的类名", 实则不然(也不知道是我理解错误?欢迎指正), 还是上例, 样式中, 设置 button
的正常 css
属性:
1 |
|
结果:
可以看到, 只有 opacity
产生了动画, transform
没有动画!(同学们可以测试下是用 animate.css
的时候, 使用 Animate.css
中相同的属性来提前设置元素, 是否还有动画效果, 欢迎提 issue
.)
transition-group
和 transition
有点不太一样, 从外观上说, transition
本身只是个包裹容器, 不参与任何的页面构成, 但是 transition-group
却会被 Vue
替换为一个标签, 默认是 span
标签, 也可以定制被替换成的标签名.
render
函数
使用 render
函数可以代替写模板的作用, 其形参 createElement
一般被写成 h
, 在组件或者标签内的各种绑定/属性等, 在 createElement
中都能找到对应的 JavaScript
写法, 如果找不到, 那就说明可以使用原生的写法, 比如 .stop
, .prevent
等, 直接使用 event.stopPropagation()
和 event.preventDefault()
即可.
其他
混合(mixin
), 就是指在写正常的组件过程中(也即在组件的生命周期中), 修改或者添加额外的功能.
部分插件
就是根据上面的 mixin
写的, 除此之外的插件还有往 Vue.prototype
添加方法, 或者通过 config
添加一些全局的方法或者属性.
路由
, 可以通过 component
的 is
属性来简单实现, 也可以直接写 render
函数根据不同路径渲染不同模板, 当然更复杂的需要第三方库了.
状态管理
, 看示例是要加个 wrap
来记录每个状态改变的过程, 然后官方建议的最佳实践是即使你能直接复制给示例属性, 但是也最好通过函数来修改状态, 因为这让状态变得可追踪.
单元测试
, 就是正常的单元测试, 没什么好说的.
服务端渲染(Server Side Render
)
看了下思路, 基本上很简单, 就是首先在 app.js
中 exports
一个 Vue
实例, 然后新建一个页面模板文件 index.html
(引入 Vue.js
和挂载 Vue
实例的方法 $mount
, 官网同样也引入了 app.js
, 是否需要待我验证下再说), 含有实例的挂载点(一个带有 id
属性的非空元素), 然后在服务端 server.js
都 require
过来, 用 vue-server-renderer
这个东西, 把 app.js
中 exports
出去的 Vue
实例渲染下, 再在返回给客户端的时候替换掉挂载点(一个带有 id
属性的非空元素, 因为 app.js
中的模板已经存在了)即可.
服务端渲染的结果就是, 会在上述的挂载点(一个带有 id
属性的非空元素)加一个 server-rendered="true"
的属性(通过右键查看页面源代码查看存在, 说明不是 js
动态添加的).
服务端也支持流式渲染, 首先需要把 html
以挂载点(一个带有 id
属性的非空元素,如 <div id="app"></div>
)为分割点, split
一下, 分为 ab
两个部分, 而刚刚使用的 vue-server-renderer
的是把 app.js
renderToString
了, 而为了支持流式渲染, 需要换个方法叫 renderToStream
, 然后监听 data
事件, 附加到, html
的 a
部分的后面, end
事件之后, 再拼接上 html
的 b
部分, 最后再一并 res.send
出去.
后记
坐标帝都, ping
cn.vuejs.org
:
ping
我司FQ
VPS
:
dig
一下:
可以看到使用的是 cloudflare
的服务, 国际网站不好做呀, 呵呵.