【Vue】【选项API】状态选项:data、props、computed、methods、watch

370 阅读20分钟

前言

首先,我们先来了解一下,状态选项有哪些?

  • data
  • props
  • computed
  • methods
  • watch
  • emits
  • expose

好了,开启正文

一、data

1.1 data是什么?

data 是用于声明组件初始响应式状态的函数

1.2 data的使用

1.2.1 data的定义方式

  • data的定义方式有
    • 对象:data: {},
    • 函数:data() { return {} },
  • Vue实例定义data:定义data 属性既可以是一个对象,也可以是一个函数
  • 组件实例定义data:只能是一个函数
    • 直接定义为一个对象,就会得到警告信息:[Vue warn]: The "data" option should be a function that returns a per-instance value in component definitions.
    • 警告说明:返回的data应该是一个函数在每一个组件实例中

1.2.2 data定义函数和对象的区别

当前有两个组件实例 componentA 和 componentB

  • data定义函数:不同组件的data属性值的对象内存地址并不相同

    如果这两个组件是用函数方式定义data。修改组件 componentA 的data属性值, componentB 的属性值不会受到任何影响

  • data定义对象:不同组件的data属性值共用了同一个内存地址,会导致组件之间相互干扰

    如果这两个组件是用对象方式定义data。修改组件 componentA 的data属性值,会发现 componentB 的属性值也发生的变化

    理由如下: componentA 和 componentB 这两个组件的data属性值共用了同一个内存地址

1.2.3 使用规则和注意事项

  • 使用规则:

    • 该函数应当返回一个普通的 JavaScript 函数,Vue会将它转换为响应式对象
    • 实例创建后,可以通过this.$data访问响应式对象
    • 组件实例也代理该数据上所有的属性,因此 this.a 等价于 this.$data.a
  • 注意事项:

    • 所有会用到的顶层数据属性都应该提前在这个对象中声明,如果该属性的值一开始获取不到,应该使用undefind 或 null 值来占位,让Vue知道这个属性是存在的。
    • 虽然理论上可以向this.$data 添加新属性,但并不推荐这么做。
    • _$ 开头的属性将不会被组件实例代理,因为它们可能和 Vue 的内置属性、API 方法冲突。你必须以 this.$data._property 的方式访问它们。
    • 推荐返回一个可能改变自身状态的对象,如浏览器 API 原生对象或是带原型的类实例等。理想情况下,返回的对象应是一个纯粹代表组件状态的普通对象。
    • 如果为data 属性使用了一个箭头函数,则 this 将不会指向组件实例,不过你仍然可以通过函数的第一个参数来访问实例
      data: (vm) => ({ a: vm.myProp })
      

使用示例:

export default {
  data() {
    return {
      a: 1,
      b: 2,
    }
  },
  created() {
    console.log(this.a) // 1
    console.log(this.$data) // { a: 1, b: 2 }
  }
}

1.3 源码分析

源码版本: 2.7.14

1.3.1 vue初始化data的代码

data的执行时机在beforeCreate之后,created之前,在initState中会调用initData。data的初始化函数 - initData主要工作:

  1. 判断 data 对象的每一个 key,不可以和 propsmethods 中的 key 相同。否则会报警告。可以看出优先级是:methods > props > data;
  2. data 设置代理,代理 vm.datavm上,就可以通过 this.XXX访问 data 上的属性了
  3. 观测 data
function initData(vm: Component) {
  let data: any = vm.$options.data
  // 如果是函数,就重新定义data;不是函数就直接取该对象或赋值空对象
  data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
  if (!isPlainObject(data)) {
    data = {}
    __DEV__ &&
      warn(
        'data functions should return an object:\n' +
          'https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
        vm
      )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  // 判断 data 对象的每一个 key,不可以和 props、methods 中的 key 相同。否则会报警告。可以看出优先级是 methods > props > data;
  while (i--) {
    const key = keys[i]
    if (__DEV__) {
      if (methods && hasOwn(methods, key)) {
        warn(`Method "${key}" has already been defined as a data property.`, vm)
      }
    }
    if (props && hasOwn(props, key)) {
      __DEV__ &&
        warn(
          `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
          vm
        )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key) // 给data设置代理,代理vm._data到vm上,就可以通过this.xxx访问_data上的属性了;
    }
  }
  // observe data
  // 观测data;
  const ob = observe(data)
  ob && ob.vmCount++
}

export function getData(data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e: any) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}

1.3.2 组件在创建时,会进行选项的合并

源码位置:src\core\instance\init.ts

自定义组件会进入mergeOptions进行选项合并

export function initMixin(Vue: typeof Component) {
  Vue.prototype._init = function (options?: Record<string, any>) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (__DEV__ && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to mark this as a Vue instance without having to do instanceof
    // check
    vm._isVue = true
    // avoid instances from being observed
    vm.__v_skip = true
    // effect scope
    vm._scope = new EffectScope(true /* detached */)
    vm._scope._vm = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options as any)
    } else { // 如果是自定义组件就会进入 mergeOptions 进行选项合并
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor as any),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (__DEV__) {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate', undefined, false /* setContext */)
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (__DEV__ && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

1.3.3 自定义组件就会进入 mergeOptions 进行选项合并

源码位置:src\core\util\options.ts

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions(
  parent: Record<string, any>,
  child: Record<string, any>,
  vm?: Component | null
): ComponentOptions {
  if (__DEV__) {
    checkComponents(child)
  }

  if (isFunction(child)) {
    // @ts-expect-error
    child = child.options
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options: ComponentOptions = {} as any
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField(key: any) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

1.3.4 定义data进行数据校验

源码位置:src\core\util\options.ts

strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): Function | null {
  if (!vm) {
    // 如果自定义组件实例的data使用非函数方式定义,就在控制台打印以下警告
    if (childVal && typeof childVal !== 'function') {
      __DEV__ &&
        warn(
          'The "data" option should be a function ' +
            'that returns a per-instance value in component ' +
            'definitions.',
          vm
        )

      return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }

  return mergeDataOrFn(parentVal, childVal, vm)
}

二、props

2.1 props是什么?

  • 官网定义:props 是用于声明一个组件的 props

  • 简单理解:就是用于组件的传参,它的主要工作就是为了接收父组件传过来的参数

  • 数据流向规则:

    单向数据流:所有的props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然而然的将状态向下流往子组件,而不是逆向传递

    如果支持逆向传递,万一子组件不小心修改了父组件的状态,会出现死循环的情况,数据流也将会很容易变得混乱而难以理解

    但是,有一种情况会以一种不明显的方式影响父组件的状态:更改对/数组类型的props。可以更改对象/数组内部的值,这是因为 JavaScript 的对象/数据是按引用传递。这种更改会允许子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解

    你想要更改一个 prop 的需求通常来源于以下两种场景:

    • prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性

      最好是新定义一个局部数据属性,从 props 上获取初始值即可:

      export default {
        props: ['initialCounter'],
        data() {
          return {
            // 计数器只是将 this.initialCounter 作为初始值
            // 像下面这样做就使 prop 和后续更新无关了
            counter: this.initialCounter
          }
        }
      }
      
    • 需要对传入的 prop 值做进一步的转换

      最好是基于该 prop 值定义一个计算属性:

      export default {
        props: ['size'],
        computed: {
          // 该 prop 变更时计算属性也会自动更新
          normalizedSize() {
            return this.size.trim().toLowerCase()
          }
        }
      }
      

2.2 props的使用

先举个栗子:

// 父组件
<template>
    <childComponent :userInfo="userInfo" />
</template>
export default {
    data() {
        return {
            userInfo: { name:'zhangsan', age: 24 }
        }
    }
}

// 子组件
<template>
    <div></div>
</template>
export default {
    // props: ['userInfo']
    props: {
        userInfo: {
            type: Object,
        }
    },
}

2.2.1 props 在父组件中是如何传?

如果一个 prop 的名字很长,应使用 camelCase 形式,因为它们是合法的 JavaScript 标识符,可以直接在模板的表达式中使用,也可以避免在作为属性 key 名时必须加上引号。

传递props我们可以按照下面两种方式划分:

1. 静态 Prop - 直接赋值

<BlogPost title="My journey with Vue" />

2. 动态 Prop - 使用v-bind或者缩写:来进行绑定的props

  • String

    <!-- 根据一个变量的值动态传入 -->
    <BlogPost :title="post.title" />
    
    <!-- 根据一个更复杂表达式的值动态传入 -->
    <BlogPost :title="post.title + ' by ' + post.author.name" />
    
  • Number

    <!-- 虽然 `42` 是个常量,我们还是需要使用 v-bind -->
    <!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
    <BlogPost :likes="42" />
    
    <!-- 根据一个变量的值动态传入 -->
    <BlogPost :likes="post.likes" />
    
  • Boolean

    <!-- 仅写上 prop 但不传值,会隐式转换为 `true` -->
    <BlogPost is-published />
    
    <!-- 虽然 `false` 是静态的值,我们还是需要使用 v-bind -->
    <!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
    <BlogPost :is-published="false" />
    
    <!-- 根据一个变量的值动态传入 -->
    <BlogPost :is-published="post.isPublished" />
    
  • Array

    <!-- 虽然这个数组是个常量,我们还是需要使用 v-bind -->
    <!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
    <BlogPost :comment-ids="[234, 266, 273]" />
    
    <!-- 根据一个变量的值动态传入 -->
    <BlogPost :comment-ids="post.commentIds" />
    
  • Object

    <!-- 虽然这个对象字面量是个常量,我们还是需要使用 v-bind -->
    <!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
    <BlogPost
      :author="{
        name: 'Veronica',
        company: 'Veridian Dynamics'
      }"
     />
    <!-- 根据一个变量的值动态传入 -->
    <BlogPost :author="post.author" />
    

3. 使用一个对象绑定多个 prop

如果你想要将一个对象的所有属性都当作 props 传入,你可以使用没有参数的 v-bind,即只使用 v-bind 而非 :prop-name。例如,这里有一个 post 对象:

export default {
  data() {
    return {
      post: {
        id: 1,
        title: 'My Journey with Vue'
      }
    }
  }
}

<BlogPost v-bind="post" />
<!-- 等价于 -->
<BlogPost :id="post.id" :title="post.title" />

2.2.2 props 在子组件中是如何接收?

所有的组件 props 都需要被显式声明。组件 props 只可以通过两种方式声明:

1. 使用字符串数组的简易形式

```ts
 export default {
     props: ['size', 'myMessage']
 }
```

2. 使用对象的完整形式

该对象的每个属性键是对应 prop 的名称,值则是该 prop 应具有的类型的构造函数,或是更高级的选项。每个prop可以进一步定义如下选项:

  • typeStringNumberBooleanArrayObjectDateFunctionSymbol、任何自定义构造函数,或由上述内容组成的数组。

    在开发模式中,Vue 会检查一个 prop 的值是否匹配其声明的类型,如果不匹配则会抛出警告。

    声明为 Boolean 类型的 props 有特别的类型转换规则:

    • prop 被声明为一种类型:传值时可以不用赋值

      // 子组件
      export default {
        props: {
          disabled: Boolean
        }
      }
      
      
      // 父组件
      <!-- 等同于传入 :disabled="true" -->
      <MyComponent disabled />
      <!-- 等同于传入 :disabled="false" -->
      <MyComponent />
      
    • prop 被声明为允许多种类型:Boolean 的转换规则也将被应用。然而,当同时允许 String 和 Boolean 时,有一种边缘情况——只有当 Boolean 出现在 String 之前时,Boolean 转换规则才适用:

      // disabled 将被转换为 true
      export default {
        props: {
          disabled: [Boolean, Number]
        }
      }
      
      // disabled 将被转换为 true
      export default {
        props: {
          disabled: [Boolean, String]
        }
      }
      
      // disabled 将被转换为 true
      export default {
        props: {
          disabled: [Number, Boolean]
        }
      }
      
      // disabled 将被解析为空字符串 (disabled="")
      export default {
        props: {
          disabled: [String, Boolean]
        }
      }
      
  • default:为该 prop 指定一个当其没有被传入或值为 undefined 时的默认值。对象或数组的默认值必须从一个工厂函数返回。工厂函数也接收原始 prop 对象作为参数。

  • required:定义该 prop 是否必需传入。在非生产环境中,如果 required 值为真值且 prop 未被传入,一个控制台警告将会被抛出。

  • validator:将 prop 值作为唯一参数传入的自定义验证函数。在开发模式下,如果该函数返回一个假值 (即验证失败),一个控制台警告将会被抛出。

export default {
  props: {
    // 类型检查
    height: Number,
    // 类型检查 + 其他验证
    age: {
      type: Number, // 
      default: 0,
      required: true,
      validator: (value) => {
        return value >= 0
      }
    },
    // 给对象设置默认值
    info: {
        type: Object,
        default: () => {
            return {
                name: "",
                age: 0,
            }
        }
    },
    // 给数组设置默认值
    arrInfo: {
        type: Array,
        default: () => {
            return [];
        }
    }
  }
}

三、computed

3.1 computed是什么?

  • 官方定义:用于声明要在组件实例上暴露的计算属性
  • 存在意义:允许我们在 Vue 组件中定义一些计算逻辑,这些逻辑会依赖于 Vue 的响应式数据,并且具有缓存机制,只有在依赖数据发生改变时才会重新计算,从而提高性能
  • 主要作用
    • 数据转换
    • 数据过滤
    • 响应式依赖
    • 缓存机制
    • 简化模板
  • 特点:
    1. 可以依赖于 Vue 实例的响应式数据。当依赖数据发生变化时,相关的计算属性就会重新计算
    2. 默认具有缓存机制。只有在相关依赖发生变化时,计算属性才会重新计算。多次访问计算属性时,Vue 会返回之前计算的结果,而不会重新执行计算函数,从而提高性能。

3.2 computed的使用

3.2.1 函数形式

computed: {
  // 函数形式的计算属性
  fullName: function () {
    return this.message.split('').reverse().join('');
  },
}

3.2.2 对象形式(getter 和 setter)

计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 getter 和 setter 来创建:

  • setter:计算属性的 setter 方法用于监视计算属性赋值时的变化,并在赋值时执行一些自定义的操作。它允许我们对计算属性进行更精细的控制

  • getter:计算属性的 getter 方法用于计算并返回一个值。它类似于一个数据属性,单其值是通过计算的出来的,getter 方法在访问计算属性时被调用

下面这个例子中:

  • 当我们通过this.fullName访问fullName时,Getter 方法会被调用,并返回完整的姓名。
  • 当我们尝试为fullName赋值时,Setter 方法会被调用,它会解析新值并相应地更新firstNamelastName
computed: {
  // 对象形式的计算属性
  fullName: {
    // getter
    get: function () {
      return this.firstName + ' ' + this.lastName;
    },
    // setter
    set: function (newValue) {
      var names = newValue.split(' ');
      this.firstName = names[0];
      this.lastName = names[names.length - 1];
    }
  }
}

注意点:

  • 不要在 getter 中做异步请求或者更改 DOM!因为一个计算属性的声明中描述的是如何根据其他值派生一个值。因此 getter 的职责应该仅为计算和返回该值。在之后的指引中我们会讨论如何使用侦听器根据其他响应式状态的变更来创建副作用。
  • 避免直接修改计算属性值!因为计算属性返回的值是派生状态。可以把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。

3.2.3 computed传参

官方文档说明中,并不支持的在computed中进行传参操作,假如现在就想在computed中进行传参操作该怎么做呢?在JavaScript语言中,我们提到过闭包这种设计,它的主要作用,不就是用来访问其他函数内部的变量,然后返回操作结果。所以,我们可以利用闭包来实现。

computed: {
  fullName: function () {
    return (val) => {
        return val.split('').reverse().join('');
    }
  },
}

3.3 computed的性能优化

在开发中我们会经常的使用到计算属性,为了确保性能,有时需要注意一些优化技巧。以下是一些计算属性的性能优化方法:

3.3.1 合理利用懒计算 - lazy

  • 作用:可以用来延迟计算属性的求职。

默认情况下,计算属性是在获取时立即求值的,但通过设置lazytrue,可以将计算属性的求值延迟到首次访问该属性时才计算。

1 延迟计算属性的使用场景

  • 计算代价较高的属性:如果计算属性的计算逻辑很复杂或需要耗费大量的计算属性,那么可以将其设置为延迟计算属性。这样,在组件初始化时不会执行这些代价较高的计算,只有在需要访问该属性时才会计算,避免不必要的性能损耗。
  • 依赖其它计算属性的计算属性:如果一个计算属性依赖了其它计算属性,而这些依赖的计算属性是在首次访问时才会进行计算的,那么将这个计算属性设置为延迟计算属性。这样,在首次访问该计算属性时,它会自动等待依赖计算属性的值可用后再再进行计算

2 使用方式

要设置计算属性的lazy选项,需要在计算属性中使用对象语法,并将lazy设置为true

computed: {
  lazyComputed: {
    get() {
      // 延迟计算的逻辑
    },
    // ...
    lazy: true
  }
}

3 Vue2 和 Vue3的使用差别

  • 只对 Vue2 有效。
  • Vue3 中已移除该选项。

在大多数情况下,默认的立即计算行为是足够的,只有在特定性能优化的场景中,才需要考虑使用延迟计算属性。

4 缺点

过度使用延迟计算属性可能会导致代码变得复杂,因此应根据实际需求进行选择和权衡。

3.3.2 合理设置缓存行为 - cache

  • 作用:用于控制计算属性的缓存行为

默认情况下,计算属性是具有缓存的。

通过设置cache选项为false,可以禁用计算属性的缓存行为,这意味着每次访问该计算属性时都会重新计算,无论其依赖项是否发生变化。

1 禁用计算属性缓存的使用场景

  • 计算属性的值可能频繁变更:如果计算属性的值可能频繁地发生变化,即使其依赖项没有变化,你可能需要禁用缓存。这样可以确保每次访问计算属性时都能获取到最新的计算结果。
  • 计算属性的值依赖于外部实例状态:如果计算属性的值依赖于外部实例状态(例如全局状态管理器或其他组件的状态),那么在这些状态发生变化时,你可能需要禁用缓存。这样可以确保计算属性能响应外部状态的变化。

2 使用方式

需要在计算属性中使用对象语法,并将cache选项设置为false

computed: {
  lazyComputed: {
    get() {
      // 延迟计算的逻辑
    },
    // ...
    cache: false
  }
}

3 Vue2 和 Vue3的使用差别

  • Vue2:默认情况下,计算属性具有缓存
  • Vue3:默认情况下,计算属性不具有缓存。如果需要保留缓存行为,可以将cache选项设置为true。需要根据实际需求进行选择和权衡。

4 缺点

禁用计算属性的缓存行为可能会导致不必要的性能开销,特别是在计算逻辑复杂或计算频率较高的情况下。因此,只在必要时禁用缓存,并确保评估其性能影响。

3.3.3 避免过度计算

尽量避免在计算属性中进行过于复杂或耗时的计算,以减轻性能负担。如果需要复杂计算,考虑将其分解为更小的计算属性或使用方法。

如果计算属性中的依赖项较多或算逻辑较复杂,可能会影响性能。在这种情况下,可以考虑使用计算属性的getter和setter方法进行优化,或者使用watch来监听多个数据的变化。

四、methods

4.1 methods是什么?

  • 官方定义:声明要混入到组件实例中的方法。

4.2 methods的使用

声明的方法可以直接通过组件实例访问,或者在模板语法中。所有的方法都会将它们的 this 上下文自动绑定为组件实例,即使在传递时也如此。

在声明方法时避免使用箭头函数,因为它们不能通过 this 访问组件实例。

export default {
  data() {
    return { a: 1 }
  },
  methods: {
    plus() {
      this.a++
    }
  },
  created() {
    this.plus()
    console.log(this.a) // => 2
  }
}

五、watch

5.1 watch是什么

  • 官方定义:用于声明在数据更改时调用的侦听回调。

会在宿主组件卸载时自动停止

5.2 watch的使用

键值名可以是数据名,也可以是里面单个嵌套属性

5.2.1 watch接收的参数

  • value: 新值
  • oldValue:旧值

5.2.2 watch的属性

  • handler:回调函数

  • immediate:在侦听器创建时立即触发回调。第一次调用时,旧值将为 undefined。值类型 Boolean;默认值 false

    Watch 是懒执行的,仅仅当数据源变化时,才会执行回调。如果需要在组件创建时立即执行一遍回调,就将immediate设置为true

  • deep:是否深度监听。值类型 Boolean;默认值 false

    Watch 默认是浅层的:被侦听的属性,仅在被赋值新值时,才会触发回调函数,而嵌套属性的变化不会触发。如果想侦听所有嵌套的变更,我们需要使用到deep

  • flush:调整回调的刷新时机。值范围:'pre' | 'post' | 'sync';默认值 'pre'

    • pre:表示在DOM更新前调用。比如需要在改变其他数据,并且这些数据改变完后一起更新DOM,从而达到提高性能的目的
    • post:表示在DOM更新完后调用。比如你要获取dom或者子组件,跟我们之前使用nextTick的意思一样
    • sync:表示同步
  • onTrack / onTrigger:调试侦听器的依赖关系

官网示例:

export default {
  data() {
    return {
      a: 1,
      b: 2,
      c: {
        d: 4
      },
      e: 5,
      f: 6
    }
  },
  watch: {
    // 侦听根级属性
    a(val, oldVal) {
      console.log(`new: ${val}, old: ${oldVal}`)
    },
    // 字符串方法名称
    b: 'someMethod',
    // 该回调将会在被侦听的对象的属性改变时调动,无论其被嵌套多深
    c: {
      handler(val, oldVal) {
        console.log('c changed')
      },
      deep: true
    },
    // 侦听单个嵌套属性:
    'c.d': function (val, oldVal) {
      // do something
    },
    // 该回调将会在侦听开始之后立即调用
    e: {
      handler(val, oldVal) {
        console.log('e changed')
      },
      immediate: true
    },
    // 你可以传入回调数组,它们将会被逐一调用
    f: [
      'handle1',
      function handle2(val, oldVal) {
        console.log('handle2 triggered')
      },
      {
        handler: function handle3(val, oldVal) {
          console.log('handle3 triggered')
        }
        /* ... */
      }
    ]
  },
  methods: {
    someMethod() {
      console.log('b changed')
    },
    handle1() {
      console.log('handle 1 triggered')
    }
  },
  created() {
    this.a = 3 // => new: 3, old: 1
  }
}

课堂提问:

1. data为什么是一个函数而不是一个对象?

是因为如果是使用对象的方式,不同组件的data属性值共用了同一个内存地址,会导致组件之间的data属性相互干扰。而且,另一方面也是因为组件不能直接访问Vue实例中定义的数据。就算是可以访问,若将所有的数据都放在Vue实例中,Vue实例就会变得非常臃肿,所以组件应该有自己保存数据的地方

源码实现上,是两种方式都可以,但是在组件进行创建的时候,会进行选项的合并,定义data会进行数据校验,此时vm实例为undefined,若data类型不是 function,救护出现警告

这不是说,data在所有情况下都不适合用对象来定义,在根实例对象data可以是对象也可以是函数,因为根实例是单例,不会产生数据污染的情况。

组件实例对象data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象

2 data里的同名数据是怎么处理的呢?

取最后被定义的那个

3. 子组件可以直接改变父组件的数据么?

按照原理上讲,是不可以!因为Vue是单向数据流规则,意思是只能是从父组件向子组件传递数据,子组件不能修改父组件的数据。

在Vue中,父组件是通过Props向子组件传递数据,如果支持修改props的数据,就会出现死循环或数据流变得更复杂,难以理解,会严重影响应用性能。当我们在子组件修改父组件的数据的时候,就会有警告,说props是只读的。

另外,我们需要注意一个特殊场景,就是对象/数组,我们可以通过修改内部数据方式不明显的修改父组件数据,这种也是会导致数据流变得复杂,所以就不要这么干。

如果说想将父组件传过来的数据作为初始值使用,并在后期可以修改的话,可以在data中定义一个数据,并将prop作为初始值赋给它;

如果说想将父组件传过来的数据进行转换,可以在 computed 中定义一个计算属性

如果想修改父组件的数据,我们可以通过$emit来让父组件去做这个变更

4. watch 和 computed 的区别?

  • 相同点:
    • watch 和 computed,本质上都是一个watcher实例,它们都通过响应式系统与数据、页面建立通信
  • 不同点:
    • 属性类型:
      • computed是计算属性
      • watch是监听属性,监听的data中数据的变化
    • 缓存:
      • computed是支持缓存。依赖的属性值发生变化,计算属性才会重新计算,否则用缓存。默认情况是支持缓存的,当然,我们可以通过属性cache来控制是否使用缓存
      • watch不支持缓存,只要监听的数据变化就会触发相应操作
    • 是否支持异步
      • computed不支持异步,因为当存在异步操作时时无法监听数据变化
      • watch可以异步操作
    • 第一次加载:
      • computed是第一次加载就监听
      • watch是第一次加载默认不监听,当immediate的值设置为true时,就会监听
    • 关键字
      • computed函数中必须有return
      • watch不需要

资料来源