【洋小洋同學】 我大膽地修改了父元件傳來的prop之後?

洋小洋同學發表於2020-04-07

瞭解Prop

基本用法

<!-- 在 HTML 中是 kebab-case 的 -->
<blog-post post-title="hello!"></blog-post>
複製程式碼
Vue.component('blog-post', {
  // 在 JavaScript 中是 camelCase 的
  props: ['postTitle'],
  template: '<h3>{{ postTitle }}</h3>'
})
複製程式碼

常見型別

字串陣列的形式

props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
複製程式碼

物件的形式

props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise // or any other constructor
}
複製程式碼

彙總

整體來說可以分為傳遞靜態的值通過v-bind 傳遞動態的值

  • 傳遞一個數字
  • 傳遞一個布林值
  • 傳入一個陣列
  • 傳入一個物件
  • 傳入一個物件的所有的屬性

post: {
  id: 1,
  title: 'My Journey with Vue'
}
複製程式碼

以下兩種方式是等價的

<blog-post v-bind="post"></blog-post>
複製程式碼
<blog-post
  v-bind:id="post.id"
  v-bind:title="post.title"
></blog-post>
複製程式碼

在 Vue 中,子元件為何不可以修改父元件傳遞的 Prop ?

嘗試修改會發生什麼事情

首先建立一個檔案來演示props 傳值(父元件的資料傳遞給子元件)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue-prop</title>
  </head>
  <body>
    <div id="app">
      {{ message }}

      <hr />
      <ol>
        <!-- 建立一個 todo-item 元件的例項 -->
        <todo-item todo="學習"></todo-item>
      </ol>
    </div>

    <script src="./vue.js"></script>
    <script>
      // 元件本質上是一個擁有預定義選項的一個 Vue 例項
      // 註冊一個TODO元件
      Vue.component("todo-item", {
        template: `
        <div>
        <li>{{todo}}</li>
        <button @click = "changeProps">嘗試改變父元件傳來的prop</button></div>`,
        props: ["todo"],
        methods: {
          changeProps() {
            console.log(`子元件的按鈕觸發`);
            this.todo = "玩耍";
          }
        }
      });
      var vm = new Vue({
        el: "#app",
        data() {
          return {
            message: "hello"
          };
        }
      });
    </script>
  </body>
</html>

複製程式碼

結果是什麼,資料也是可以修改成功的,但是控制檯會報一個警告

vue.js:634 [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "todo"
複製程式碼

微信截圖_20200407105110.png

單向資料流

所有的 prop 都使得其父子 prop 之間形成了一個單向下行繫結:父級 prop 的更新會向下流動到子元件中,但是反過來則不行。這樣會防止從子元件意外改變父級元件的狀態,從而導致你的應用的資料流向難以理解。

額外的,每次父級元件發生更新時,子元件中所有的 prop 都將會重新整理為最新的值。這意味著你不應該在一個子元件內部改變 prop。如果你這樣做了,Vue 會在瀏覽器的控制檯中發出警告。

簡單的來說,vue這樣處理從父元件來的資料,是為了方便監測資料的流動,如果一旦出現的錯誤,可以更為迅速的定位到錯誤的位置,

什麼情況下,我們會改變這個prop

props: ['initialCounter'],
data: function () {
  return {
    counter: this.initialCounter
  }
}
複製程式碼
  • 第一種情況:這個 prop 用來傳遞一個初始值;這個子元件接下來希望將其作為一個本地的 prop 資料來使用。在這種情況下,最好定義一個本地的 data 屬性並將這個 prop 用作其初始值。藉助data
props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}
複製程式碼
  • 第二種情況這個 prop 以一種原始的值傳入且需要進行轉換。在這種情況下,最好使用這個 prop 的值來定義一個計算屬性藉助計算屬性

如果修改了,Vue 是如何監控到屬性的修改並給出警告的

這裡我們可以去原始碼裡找答案,畢竟真實的警告暗示是vue來給出的

src>core>instance>state.js // 原始碼的位置
複製程式碼

function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  // 快取prop的keys 為了是將來更新的props可以使用陣列進行迭代,而不是動態的物件列舉
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  // 不是root根元件
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    // 通過判斷是否在開發環境
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      // 如果不是,說明此修改來自子元件,觸發warning提示
      /**
       * 傳入的第4個函式是自定義的set函式,當props被修改的時候就會觸發第四個引數的函式
       */
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      // 如果是開發環境,會在觸發Set的時候判斷是否此key是否處於updatingChildren中被修改
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}
複製程式碼
src>core>observer>index.js
複製程式碼
/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
複製程式碼

思考

如果是傳入的是引用的資料型別,控制檯會警告嘛?

 <todo-item todo="學習" :todolist="todolist"></todo-item>
複製程式碼
   var vm = new Vue({
        el: "#app",
        data() {
          return {
            message: "hello",
            todolist: [
              {
                id: "1",
                todo: "吃飯"
              }
            ]
          };
        }
      });
複製程式碼

02.png

最後

如有自己的理解也可以在評論區一塊學習,如有錯誤也請指出,感謝你讀到這裡~~

推薦閱讀

相關文章