深入了解v-model流程

v-model原理

vue中v-model是一個語法糖,所謂的語法糖就是對其他基礎功能的二次封裝而產生的功能。簡單點說,v-model本身就是父組件對子組件狀態以及狀態改變事件的封裝。其實現原理上分為兩個部分:

  • 通過props設置子組件的狀態
  • 通過監聽子組件發出的事件改變父組件的狀態,從而影響子組件的props值

通過以上兩個部分,實現了父組件的狀態和子組件狀態進行了綁定的效果。

demo

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>v-model示例</title>
        <script type="text/javascript" src="vue.js"></script>
    </head>

    <body>
        <div id="app">
            <div>這裡是父組件的狀態:</div>
            <div style="margin-bottom: 15px;">{{content}}</div>
            <Child v-model="content"></Child>
        </div>

        <template id="input">
            <div>
                <div>這裡是子組件的輸入區域:</div>
                <input :value="value" @input="contentChange" />
            </div>
        </template>

        <script type="text/javascript">
        var Child = {
            template: "#input",
            props: {
                value: {
                    type: String,
                    required: true
                }
            },
            methods: {
                contentChange(value){
                    this.$emit("input", value.target.value);
                }
            }
        };

        var vueInstance = new Vue({
            el: "#app",
            components: {Child},
            data: {
                content: ""
            }
        })
        </script>
    </body>
</html>

在瀏覽器中打開上述html頁面,可以看到實時效果:在子組件中的input框中輸入內容可以在父組件區域實時顯示,達到了子組件中狀態和父組件狀態實時綁定的效果。

修改v-model默認監聽的事件和設置prop的名稱

v-model指令默認是在子組件上設置的prop名稱是value,默認監聽子組件上的input事件,在上面的demo上,如果我們修改子組件contentChange函數中發出的事件名稱,在父組件中就無法實時獲取到子組件的輸入。

Vue中提供了通過在子組件上定義model屬性來修改這兩個參數名稱的功能,不過該功能需要在版本2.2以上才能使用,如下demo所示:

demo

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>v-model示例</title>
        <script type="text/javascript" src="vue.js"></script>
    </head>

    <body>
        <div id="app">
            <div>這裡是父組件的狀態:</div>
            <div style="margin-bottom: 15px;">{{content}}</div>
            <Child v-model="content"></Child>
        </div>

        <template id="input">
            <div>
                <div>這裡是子組件的輸入區域:</div>
                <input :value="content" @input="contentChange" />
            </div>
        </template>

        <script type="text/javascript">
        var Child = {
            template: "#input",
            model: {
                prop: "content",
                event: "contentChanged"
            },
            props: {
                content: {
                    type: String,
                    required: true
                }
            },
            methods: {
                contentChange(value){
                    this.$emit("contentChanged", value.target.value);
                }
            }
        };

        var vueInstance = new Vue({
            el: "#app",
            components: {Child},
            data: {
                content: ""
            }
        })
        </script>
    </body>
</html>

Vue中對v-model指令處理分析

基於Vue2.0版本,分析我們在標籤上寫上v-model屬性到vue組件實現響應的流程。

解析部分

在將HTML解析稱AST時,會解析HTML中標籤的屬性
function processAttrs(el){
  ...
  name = name.replace(dirRE, '')
  // parse arg
  const argMatch = name.match(argRE)
  if (argMatch && (arg = argMatch[1])) {
    name = name.slice(0, -(arg.length + 1))
  }
  addDirective(el, name, value, arg, modifiers)
  ...
}

提取指令的名稱,v-model的指令名稱name為model,然後添加到實例的指令中

將指令相關內容添加到實例指令中
export function addDirective (
  el: ASTElement,
  name: string,
  value: string,
  arg: ?string,
  modifiers: ?{ [key: string]: true }
) {
  (el.directives || (el.directives = [])).push({ name, value, arg, modifiers })
}

在實例的指令屬性中添加相應的指令,這樣就實現了從html上的屬性到Vue實例上指令格式的轉換

指令設置部分

在將html解析稱AST之後,實例對應的directives屬性上就有了我們設置的v-model相關的值,包括參數值value,name是model

調用指令的構造函數
function genDirectives (el: ASTElement): string | void {
  const dirs = el.directives
  if (!dirs) return
  let res = 'directives:['
  let hasRuntime = false
  let i, l, dir, needRuntime
  for (i = 0, l = dirs.length; i < l; i++) {
    dir = dirs[i]
    needRuntime = true
    const gen = platformDirectives[dir.name] || baseDirectives[dir.name]
    if (gen) {
      // compile-time directive that manipulates AST.
      // returns true if it also needs a runtime counterpart.
      needRuntime = !!gen(el, dir, warn)
    }
    ...
}

在v-model指令的構造函數中會根據tag的種類進行不同的創建函數進行創建,如果我們自定義指令需要在子組件上添加屬性,也需要在這個函數裏面進行操作

普通tag下的v-model指令構造過程
function genDefaultModel 
  el: ASTElement,
  value: string,
  modifiers: ?Object
): ?boolean {
  ...
  addProp(el, 'value', isNative ? `_s(${value})` : `(${value})`)
  addHandler(el, event, code, null, true)
  ...
}
  • addProp在el上設置一個名稱為value的prop,同時設置其值
  • addHandler在el上設置事件處理函數

指令響應變化部分

createPatchFunction統一處理指令的鉤子函數

createPatchFunction函數返回一個patch函數,在patch處理過程中,會調用指令的鉤子函數,包括:

  • bind
  • inserted
  • update
  • componentUpdated
  • unbind

總結

編譯過程

  1. 從html上解析所設置的指令
  2. 通過gen*函數將指令設置到AST上
  3. 調用指令的構造函數,設置指令需要在編譯時期處理的事情

初始化過程

通過在patch函數中,調用統一的鉤子函數,觸髮指令的鉤子函數,實現相應的功能