一些没什么卵用的Vue知识

222 阅读1分钟

前言

废话不多说,直接进入正题!

保留关键字

在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.

src=http___wx3.sinaimg.cn_large_006i487Uly1fiaxd5bjj0j305i04e3ye.jpg&refer=http___wx3.sinaimg.webp