
当一个 Vue 实例创建时,Vue 会遍历 data 中的属性,用 Object.defineProperty 将它们转为 getter / setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty 来劫持各个属性的 setter 和 getter,在数据变动时发布消息给订阅者,触发相对应的监听回调。主要分以下步骤:
setter 和 getter,这样给这个对象的某个值赋值,就会触发 setter,那么就能监听到数据变化;update() 方法;dep.notice() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调数据变化 -> 视图更新 和 视图交互变化 -> 数据 Model 变更的双向绑定效果。
不会立即同步执行渲染。
Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的时间循环 tick 中,Vue 刷新队列并执行实际工作。
v-if 和 v-for 同级
<div id="demo">
<h1>v-if 和 v-for 哪个优先级更高?如果两个同时出现,应该如何优化?</h1>
<p v-for="child in children" v-if="isFolder">{{child.title}}</p>
</div>
// app.$options.render
// 循环并对每一个循环子项做判断
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"demo"}},[_c('h1',[_v("v-if 和 v-for 哪个优先级更高?如果两个同时出现,应该如何优化?")]),_v(" "),_l((children),function(child){return (isFolder)?_c('p',[_v(_s(child.title))]):_e()})],2)}
})
v-if 和 v-for 嵌套
<div id="demo">
<h1>v-if 和 v-for 哪个优先级更高?如果两个同时出现,应该如何优化?</h1>
<template v-if="isFolder">
<p v-for="child in children">{{child.title}}</p>
</template>
</div>
// app.$options.render
// 如果判断不通过就不展开循环
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"demo"}},[_c('h1',[_v("v-if 和 v-for 哪个优先级更高?如果两个同时出现,应该如何优化?")]),_v(" "),(isFolder)?_l((children),function(child){return _c('p',[_v(_s(child.title))])}):_e()],2)}
})
产生原因
// src\compiler\codegen\index.js
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
// ...
}
}
结论
补充
<div id="demo">
<h1>Vue组件data为什么必须是个函数而Vue的根实例则没有限制?</h1>
<comp></comp>
<comp></comp>
</div>
<script>
Vue.component('comp', {
template: '<div @click="counter++">{{counter}}</div>',
data: { counter: 0 }
})
const app = new Vue({
el: '#demo',
});
</script>

data使用逻辑
// src\core\instance\state.js
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// ...
}
结论
<div id="demo">
<p v-for="item in items" :key="item">{{item}}</p>
</div>
<script>
const app = new Vue({
el: '#demo',
data: {
items: ['a', 'b', 'c', 'd', 'e'],
},
mounted () {
setTimeout(() => {
this.items.splice(2, 0, 'f')
}, 1000)
}
});
</script>
// src\core\vdom\patch.js
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
key与旧节点进行对比,然后找出差异必要性 每个组件对应一个watcher,组件中可能存在很多个data中的key的使用,为了在执行过程中精确知道谁在发生变化,需要使用diff比较
// src\core\instance\lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
// ...
return vm
}
执行方式 patchVnode()是diff发生的地方,整体策略是:深度优先,同层比较

// src\core\vdom\patch.js
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// ...
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
高效性
// src\core\vdom\patch.js
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
总结
源码分析
Vue.component('comp', {
template: '<div>this is a component</div>'
})
// 具体实现见
// src\core\global-api\assets.js
// src\core\global-api\extend.js
单文件组件:vue-loader会编译template为render函数,最终导出的依然是组件配置对象
<template>
<div>this is a component</div>
</template>
总结






利用 Object.defineProperty() 劫持对象的访问器,在属性值发生发生变化时可以获取变化,然后根据变化进行后续响应。在 Vue3.0 中通过 Proxy 代理对象进行类似的操作。
// 要劫持的对象
const data = {
name: '',
};
function say(name) {
if (name === '古天乐') {
console.log('给大家推荐一款好玩的游戏');
} else if (name === '渣渣辉') {
console.log('戏我养过很多,可游戏我只玩贪玩蓝月');
} else {
console.log('系兄弟就来砍我');
}
}
// 遍历对象,对其属性值进行劫持
Object.keys(data).forEach(function (key) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
console.log('get');
},
set: function(newVal) {
console.log(`大家好,唔系${newVal}`);
say(newVal);
},
});
});
data.name = '渣渣辉';
// 大家好,唔系渣渣辉
// 戏我养过很多,可游戏我只玩贪玩蓝月
Proxy 优势
Object.defineProperty 只能遍历对象属性直接修改现代前端框架有两种方式侦测变化,一种是 pull ,一种是 push
setState API 显示更新,然后 React 会进行一层层的 Virtual DOM Diff 操作找出差异,然后 Patch 到 DOM 上根本原因是 Vue 与 React 的变化侦测方式有所不同
React 是 pull 的方式侦测变化,当 React 知道发生变化后,会使用 Virtual DOM Diff 进行差异检测,但是很多组件实际上是肯定不会发生变化的,此时就需要用 shouldComponentUpdate 进行手动操作来减少 diff,从而提高程序整体的性能。
Vue 是 pull + push 的方式侦测变化的,在一开始就知道哪个组件发生了变化,因此在 push 阶段并不需要手动控制 diff,而组件内部采用的 diff 方式实际上是可以引入类似于 shouldComponentUpdate 相关生命周期的,但是通常合理大小的组件不会有过量的 diff,手动优化的价值有限,因此目前 Vue 并没有考虑引入 shouldComponentUpdate 这种手动优化的生命周期。
Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模板、挂载 DOM -> 渲染、更新 -> 渲染、卸载等一系列过程。
生命周期 | 描述 |
|---|---|
beforeCreate | 组件实例被创建之初,组件的属性生效之前(data 和 methods 中的数据还没有初始化) |
created | 组件实例已经完全创建,属性也绑定,但真实 DOM 还没有生成,$el 还不可用(data 和 methods 都已经初始化好了,可以进行操作) |
beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用(模板已经编译好,但尚未挂载到页面中去) |
mounted | el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子 |
beforeUpdate | 组件数据更新之前调用,发生在 VDOM 打补丁之前 |
update | 组件数据更新之后 |
activated | keep-alive 专属,组件被激活时调用 |
deactivated | keep-alive 专属,组件被销毁时调用 |
beforeDestroy | 组件销毁前调用 |
destroyed | 组件销毁后调用 |
路由懒加载
const router = new VueRouter({
routes: [
{ path: '/foo', component: () => import('./Foo.vue') }
]
})
keep-alive缓存页面
<template>
<div id="app">
<keep-alive>
<router-view/>
</keep-alive>
</div>
</template>
使用v-show复用DOM
<template>
<div class="cell">
<!-- 这种情况使用v-show复用DOM比v-if效果好 -->
<div v-show="value" class="on">
<Heavy :n="10000" /><!-- 超级大组件 -->
</div>
<section v-show="!value" class="off">
<Heavy :n="10000" />
</section>
</div>
</template>
v-for遍历避免同时使用v-if
<template>
<ul>
<li
v-for="user in activeUsers"
:key="user.id">
{{user.name}}
</li>
</ul>
</template>
<script>
export default {
computed: {
activeUsers: function() {
return this.users.filter(function(user) {
return user.isActive
})
}
}
}
</script>
长列表性能优化
如果列表是纯粹的数据展示,不会有任何改变,就不需要做响应化
export default {
data: () => ({
users: []
}),
async created() {
const users = await axios.get('/api/users')
this.users = Object.freeze(users)
}
}如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容
<recycle-scroll
class="items"
:items="items"
:item-size="24">
<template v-slot="{item}">
<FetchItemView
:item="item"
@vote="voteItem(item)"/>
</template>
</recycle-scroll>
参考vue-virtual-scroller、vue-virtual-scroll-list
事件的销毁 Vue组件销毁时会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件
create() {
this.timer = setInterval(this.refresh, 2000)
},
beforeDestroy() {
clearInterval(this.timer)
}
图片懒加载 对于图片过多的页面,为了加速页面加载速度,很多时候需要将页面内未出现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载
<img v-lazy="/static/img/1.png">
参考vue-lazyload
第三方插件按需引入
import Vue from 'vue'
import { Button, Select } from 'element-ui'
Vue.use(Button)
Vue.use(Select)
无状态的组件标记为函数式组件
<template functional>
<div class="cell">
<div v-if="props.value" class="on"></div>
<section v-else class="off"></section>
</div>
</template>
<script>
export default {
props: ['value']
}
</script>
子组件分割
<template>
<div>
<ChildComp>
</div>
</template>
<script>
export default {
components: {
ChildComp: {
methods: {
heavy() {/* 耗时任务 */}
},
render (h) {
return h('div', this.heavy())
}
}
}
}
</script>变量本地化
<template>
<div :style="{opacity: start / 300 }">
{{result}}
</div>
</template>
<script>
import { heavy } from '@/utils'
export default {
props: ['start'],
computed: {
base() {
return 42
},
result() {
const base = this.base // 不要频繁引用this.base
let result = this.start
for (let i = 0; i < 1000; i++) {
result += heavy(base)
}
return result
}
}
}
</script>
SSR
slot 又名插槽,是 Vue 的内容分发机制,组件内部的模板引擎使用 slot 元素作为承载分发内容的出口。插槽 slot 是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定。
slot 分三类:
实现原理:当子组件 vm 实例化时,获取到父组件传入的 slot 标签的内容,存放在 vm.slots 中,默认插槽为 vm.slot.default,具名插槽为 vm.slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到 slot 标签,使用 slot 中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可以称该插槽为作用域插槽。
使用 Vue.mixin 全局混入 mixins是一种分发Vue组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项,当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。mixins选项接受一个混合对象的数组
<template>
<div id="app">
<p>num: {{num}}</p>
<button @click="add">add</button>
</div>
</template>
<script>
var addLog = {
updated: function() {
console.log('数据发生变化' + this.num)
}
}
export default {
name: 'app',
data() {
return {
num: 1
}
},
methods: {
add() {
this.num++
}
},
updated() {
console.log('原生updated')
},
mixins: [addLog] // 混入
}
</script>全局混入
// src\main.js
Vue.mixin({
updated: function() {
console.log('全局混入')
}
})
调用顺序:混入对象的钩子将在组件自身钩子之前调用,如果遇到全局混入,全局混入的执行要早于混入和组件里的方法
加 slot 扩展
默认插槽和匿名插槽 slot用来获取组件中的原内容
<template id="hello">
<div>
<h1>slot</h1>
<slot>如果没有原内容就显示该内容</slot><!-- 默认插槽 -->
</div>
</template>
<script>
var vm = new Vue({
el: '#app',
components: {
'my-hello': {
template: '#hello'
}
}
})
</script>
具名插槽
<div id="app">
<my-hello>
<ul slot="s1">
<li>aaa</li>
<li>bbb</li>
<li>ccc</li>
</ul>
<ol slot="s2">
<li>111</li>
<li>222</li>
<li>333</li>
</ol>
</my-hello>
</div>
<template id="hello">
<div>
<slot name="s2"></slot>
<h3>具名插槽</h3>
<slot name="s1"></slot>
</div>
</template>
<script>
var vm = new Vue({
el: '#app',
components: {
'my-hello': {
template: '#hello'
}
}
})
</script>
区别
定义、语义区别 watch
var vm = new Vue({
el: '#app',
data: {
foo: 1
},
watch: {
foo: function(newVal, oldVal) {
console.log(newVal + '-' +oldVal)
}
}
})
vm.foo = 2 // 2 - 1
computed
var vm = new Vue({
el: '#app',
data: {
firstName: 'Foo',
lastName: 'Bar',
}.
computed: {
fullName: function() {
return this.firstName + ' ' + this.lastName
}
}
})
vm.fullName // Foo Bar
computed
watch
props $emit 或本组件的值,当数据变化时来执行回调进行后续操作功能区别 watch更通用,computed派生功能都能实现,计算属性底层来自于watch,但是做了更多,例如缓存
用法区别 computed更简单高效,优先使用 有些必须watch,比如值变化后要和后端交互
使用场景 watch:需要在数据变化时执行异步或开销较大的操作时使用,简单讲,当一条数据影响多条数据的时候,如搜索数据 computed:对于任何复杂逻辑或一个数据属性在它所依赖的属性发生变化时,也要发生变化,简单讲,当一个属性受多个属性影响的时候,如购物车商品结算时
它可以在 DOM 更新完毕之后执行一个回调,以此来确保我们操作的是更新后的 DOM 。
实现原理: