前言
废话不多说,直接进入正题!
保留关键字
在Vue中定义$或者_开头的属性时,不能够直接通过this来进行访问,并且在dev模式下控制台还会出现提示
<template>
<span>{{ _whatever }}</span>
</template>
<script>
// [Vue warn]: Property "p" must be accessed with "$data.p" because properties starting with "$" or "_" are not proxied in the Vue instance to prevent conflicts with Vue....
export default {
data () {
return {
_whatever: true
}
}
}
</script>
原因:当你在data里面定义了以$或者_开头的属性时,Vue并不会将它代理到当前vm上
// 源码路径:src\core\instance\state.js
function initData (vm: Component) {
// proxy data on instance
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
if (props && hasOwn(props, key)) {
} else if (!isReserved(key)) { // 如果是保留字则不将它代理到this上
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
export function isReserved (str: string): boolean {
const c = (str + '').charCodeAt(0)
// 分别表示$和_的UTF-16字符
return c === 0x24 || c === 0x5F
}
// 这里则将会代理data属性到this上
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
如果我硬是要以这两个字符作为属性的开头呢?那就只能按照提示通过$data.xx或者_data.xx来进行访问了
before选项
watch中有一个比较特殊的选项叫before,它会在watch发生变更前进行调用
<template>
<span @click="changeKey">{{ key }}</span>
</template>
<script>
export default {
data () {
return {
key: 1
}
},
watch: {
key: {
handler (val) {
console.log('now val is ', val)
},
before () {
console.log('key has been changed')
}
}
},
methods: {
changeKey () {
this.key = 2
this.key = 3
this.key = 1
}
}
}
</script>
比如在上面的例子中,before函数会被调用,而handler函数则不会被调用。如果我们想知道某个属性是否发生过变更,则可以通过before选项来进行监听,无论最终它的值是否有发生变化,我都能知道它是否在中间的过程中被人篡改过了
// 源码位置:src\core\observer\watcher.js
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// options
if (options) {
// 就是这里了before函数!
this.before = options.before
}
}
// 源码位置:src\core\observer\scheduler.js
function flushSchedulerQueue () {
let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
// 调用watch前先调用before
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
}
}
从上面的代码中可以看到,before函数是在flush里边进行调用的,也就是说当watch的sync选项设置为true,那么before函数也就没有什么卵用了
watch一个function
如果watch监听一个函数会发生什么?什么?watch还能监听一个函数?!!是的,watch可以监听一个函数
<template>
<span @click="changeKey">click to change key</span>
</template>
<script>
export default {
data () {
return {
key: 1,
otherKey: 2
}
},
methods: {
changeKey () {
const changeId = Math.random() > 0.5 ? 'otherKey' : 'key'
this[changeId]++
}
},
created() {
const watchFn = () => {
console.log('cur data is ', this.key, this.otherKey)
}
// 传入函数作为监听的属性
this.$watch(watchFn)
}
}
</script>
以上代码中,每当key或者otherKey发生改变时,它都会执行watchFn,从而达到了监听多个参数的效果
// 源码位置:src\core\observer\watcher.js
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
// 这时候会直接执行当前监听的函数了
value = this.getter.call(vm, vm)
} catch (e) {
} finally {
popTarget()
this.cleanupDeps()
}
return value
}
}
可以看到当传递一个函数时,则getter会引用该函数,当它执行get的时候则会直接执行该方法并收集到相关的依赖属性。而当属性发生变化时,又会调用get方法从而执行了getter函数,也就是所传递的函数
问:能不能直接在$options上直接监听一个函数?
答:不能,因为watch的属性key只能是对象所允许的key类型(string/number/symbol),因此不能使用function
题外话:render函数的update原理正是基于该属性以及before option的,有兴趣的同学可以自行查看$mount的实现
options与合并策略
有些属性并不需要具备响应式,因为它们从来都不会改变,你只是想要在模板上使用它来进行展示或者判断,那么将属性挂载到$options上也是一个不错的选择。但是出现需要使用到mixins的时候,你可能会发现它和你所想要的不一样
// mixOptions
export default {
keys: ['a', 'b', 'c']
}
import mixOptions from "./mixOptions";
export default {
mixins: [mixOptions],
keys: ['d', 'e', 'f'],
created() {
console.log('this $options keys is ', this.$options.keys)
}
}
比如上面的例子中,我想要输出的结果是['a', 'b', 'c', 'd', 'e', 'f'],然而结果却输出了['d', 'e', 'f']
这时候你可以通过自定义Vue.config.optionMergeStrategies.keys来实现
Vue.config.optionMergeStrategies.keys = function (parentVal = [], childVal = []) {
return [...parentVal, ...childVal]
}
// 源码位置:src\core\util\options.js
const defaultStrat = function (parentVal: any, childVal: any): any {
// 默认策略为如果child存在则使用child,否则才使用mixins的值
return childVal === undefined
? parentVal
: childVal
}
// 合并选项策略
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
注:你也可以使用Object.freeze来使得你的属性是非响应式的
computed与$set
如果你在对一个computed属性进行$set的时候,你会发现这个属性并不具有响应性
<template>
<div @click="cloneData.otherKey++">
{{ cloneData.otherKey }}
</div>
</template>
<script>
export default {
data () {
return {
originData: {
originKey: 1
}
}
},
computed: {
cloneData ({originData}) {
return {
...originData,
extendKey: 2
}
}
},
created() {
this.$set(this.cloneData, 'otherKey', 3)
}
}
这是因为computed看起来虽然是响应式的,但是它的响应性并不是自身实现的,而是由它所依赖的属性来实现的。因此当你对一个非响应式对象调用this.$set是不会具有响应性效果的
// 源码位置:src\core\observer\index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
const ob = (target: any).__ob__
// 因为computed对象并不是真正的响应式对象,因此它的__ob__属性为undefined,所以直接return掉了
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
warn
用腻了console.log? 不如试一下Vue.util.warn,它可以使得你的打印和Vue的错误日志保持一致。除此之外,它还可以让你更加快速的找到日志打印的位置(可以展开看到调用栈)
Vue.util.warn('oh someth error!')
// 控制台将会打印出[Vue warn]: oh someth error以及调用栈信息
要是你觉得它的打印不符合你的审美,你也可以通过Vue.config.warnHandler进行样式定制
// 源码位置:src\core\util\debug.js
warn = (msg, vm) => {
const trace = vm ? generateComponentTrace(vm) : ''
if (config.warnHandler) {
config.warnHandler.call(null, msg, vm, trace)
} else if (hasConsole && (!config.silent)) {
// 默认的打印策略
console.error(`[Vue warn]: ${msg}${trace}`)
}
}
每次都要引入Vue才能调用,烦死了!不如直接把它挂到prototype上(危!)
defineReactive
如果说你想要在多个兄弟组件之间传递属性且要求它具有响应式,同时该属性只会在兄弟组件中使用。脑海里过滤了一下八股文中常提到的几种办法
0.xxStorage:没有响应式,不考虑
1.provide/inject:会污染父组件,不大合适
2.Vuex:全局共享属性,不考虑
3.eventBus:具有响应性,并且并非全局的,nice!
单看以上几种方法,好像eventBus是相对比较妥的,但是这意味着你要创建一个Vue对象,有没有什么办法只是让我的某个属性具有响应性呢?你说的这不就是defineReactive么!
// 这里是定义共享属性
import Vue from 'vue'
export const reactiveObj = Object.create(null)
Vue.util.defineReactive(reactiveObj, 'num', 1)
// 以下是使用
<template>
<div @click="addLiteNum">
{{ fromLiteNum }}
</div>
</template>
<script>
import { reactiveObj } from './reactiveObj'
export default {
computed: {
fromLiteNum () {
return reactiveObj.num
}
},
methods: {
addLiteNum () {
reactiveObj.num++
}
}
}
</script>
使用defineReactive可以使得你创建一个具有响应式的属性,同时又不必创建一个Vue对象,岂不美哉!(说得好,我选择Vue.observable)
等等,好像有注释来着
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.