前端框架面試之 Vue 的使用 和 Vue 的原理萬字長文總結

一、 Vue 的使用

  1. vue 基本知識點,如下所示:
  • 插值、表達式,指令、動態屬性,v-html 會有 XSS 風險,會覆蓋子組件,代碼如下所示:
<template>
    <div>
        <p>文本插值 {{message}}</p>
        <p>JS 表達式 {{ flag ? 'yes' : 'no' }} (只能是表達式,不能是 js 語句)</p>

        <p :id="dynamicId">動態屬性 id</p>

        <hr/>
        <p v-html="rawHtml">
            <span>有 xss 風險</span>
            <span>【注意】使用 v-html 之後,將會覆蓋子元素</span>
        </p>
        <!-- 其他常用指令後面講 -->
    </div>
</template>

<script>
export default {
    data() {
        return {
            message: 'hello vue',
            flag: true,
            rawHtml: '指令 - 原始 html <b>加粗</b> <i>斜體</i>',
            dynamicId: `id-${Date.now()}`
        }
    }
}
</script>

  • computedwatchcomputed 有緩存,data 不變則不會重新計算。watch 可以進行深度監聽,watch 監聽引用類型,拿不到 oldVal,代碼如下所示:
    • computed 的代碼:
      <template>
          <div>
              <p>num {{num}}</p>
              <p>double1 {{double1}}</p>
              <input v-model="double2"/>
          </div>
      </template>
      
      <script>
      export default {
          data() {
              return {
                  num: 20
              }
          },
          computed: {
              double1() {
                  return this.num * 2
              },
              double2: {
                  get() {
                      return this.num * 2
                  },
                  set(val) {
                      this.num = val/2
                  }
              }
          }
      }
      </script>
      
      • watch 的代碼:
      <template>
          <div>
              <input v-model="name"/>
              <input v-model="info.city"/>
          </div>
      </template>
      
      <script>
      export default {
          data() {
              return {
                  name: '張三',
                  info: {
                      city: '上海'
                  }
              }
          },
          watch: {
              name(oldVal, val) {
                  // eslint-disable-next-line
                  console.log('watch name', oldVal, val) // 值類型,可正常拿到 oldVal 和 val
              },
              info: {
                  handler(oldVal, val) {
                      // eslint-disable-next-line
                      console.log('watch info', oldVal, val) // 引用類型,拿不到 oldVal 。因爲指針相同,此時已經指向了新的 val
                  },
                  deep: true // 深度監聽
              }
          }
      }
      </script>
      
  • classstyle,使用動態屬性,使用駝峯式寫法,代碼如下所示:
<template>
    <div>
        <p :class="{ black: isBlack, yellow: isYellow }">使用 class</p>
        <p :class="[black, yellow]">使用 class (數組)</p>
        <p :style="styleData">使用 style</p>
    </div>
</template>

<script>
export default {
    data() {
        return {
            isBlack: true,
            isYellow: true,

            black: 'black',
            yellow: 'yellow',

            styleData: {
                fontSize: '40px', // 轉換爲駝峯式
                color: 'red',
                backgroundColor: '#ccc' // 轉換爲駝峯式
            }
        }
    }
}
</script>

<style scoped>
    .black {
        background-color: #999;
    }
    .yellow {
        color: yellow;
    }
</style>
  • 條件渲染,v-ifv-else 的用法,可使用變量,也可以使用 === 表達式,需要區分 v-ifv-show 的區別和使用場景,代碼如下所示:
<template>
    <div>
        <p v-if="type === 'a'">A</p>
        <p v-else-if="type === 'b'">B</p>
        <p v-else>other</p>

        <p v-show="type === 'a'">A by v-show</p>
        <p v-show="type === 'b'">B by v-show</p>
    </div>
</template>

<script>
export default {
    data() {
        return {
            type: 'a'
        }
    }
}
</script>
  • 循環列表渲染,遍歷對象可以使用 v-forkey 也很重要,不能夠亂寫。注意的是,v-forv-if 是不能一起使用的,代碼如下所示:
<template>
    <div>
        <p>遍歷數組</p>
        <ul>
            <li v-for="(item, index) in listArr" :key="item.id">
                {{index}} - {{item.id}} - {{item.title}}
            </li>
        </ul>

        <p>遍歷對象</p>
        <ul >
            <li v-for="(val, key, index) in listObj" :key="key">
                {{index}} - {{key}} -  {{val.title}}
            </li>
        </ul>
    </div>
</template>

<script>
export default {
    data() {
        return {
            flag: false,
            listArr: [
                { id: 'a', title: '標題1' }, // 數據結構中,最好有 id ,方便使用 key
                { id: 'b', title: '標題2' },
                { id: 'c', title: '標題3' }
            ],
            listObj: {
                a: { title: '標題1' },
                b: { title: '標題2' },
                c: { title: '標題3' },
            }
        }
    }
}
</script>
  • 事件,event 參數,自定義參數。事件修飾符,按鍵修飾符,觀察事件被綁定到哪裏,代碼如下所示:
<template>
    <div>
        <p>{{num}}</p>
        <button @click="increment1">+1</button>
        <button @click="increment2(2, $event)">+2</button>
    </div>
</template>

<script>
export default {
    data() {
        return {
            num: 0
        }
    },
    methods: {
        increment1(event) {
            // eslint-disable-next-line
            console.log('event', event, event.__proto__.constructor) // 是原生的 event 對象
            // eslint-disable-next-line
            console.log(event.target)
            // eslint-disable-next-line
            console.log(event.currentTarget) // 注意,事件是被註冊到當前元素的,和 React 不一樣
            this.num++

            // 1. event 是原生的
            // 2. 事件被掛載到當前元素
            // 和 DOM 事件一樣
        },
        increment2(val, event) {
            // eslint-disable-next-line
            console.log(event.target)
            this.num = this.num + val
        },
        loadHandler() {
            // do some thing
        }
    },
    mounted() {
        window.addEventListener('load', this.loadHandler)
    },
    beforeDestroy() {
        //【注意】用 vue 綁定的事件,組建銷燬時會自動被解綁
        // 自己綁定的事件,需要自己銷燬!!!
        window.removeEventListener('load', this.loadHandler)
    }
}
</script>
  • 表單,v-model。常見的表單項 textarea、checkbox、radio、select,修飾符 lazy、number、trim,代碼如下所示:
<template>
    <div>
        <p>輸入框: {{name}}</p>
        <input type="text" v-model.trim="name"/>
        <input type="text" v-model.lazy="name"/>
        <input type="text" v-model.number="age"/>

        <p>多行文本: {{desc}}</p>
        <textarea v-model="desc"></textarea>
        <!-- 注意,<textarea>{{desc}}</textarea> 是不允許的!!! -->

        <p>複選框 {{checked}}</p>
        <input type="checkbox" v-model="checked"/>

        <p>多個複選框 {{checkedNames}}</p>
        <input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
        <label for="jack">Jack</label>
        <input type="checkbox" id="john" value="John" v-model="checkedNames">
        <label for="john">John</label>
        <input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
        <label for="mike">Mike</label>

        <p>單選 {{gender}}</p>
        <input type="radio" id="male" value="male" v-model="gender"/>
        <label for="male"></label>
        <input type="radio" id="female" value="female" v-model="gender"/>
        <label for="female"></label>

        <p>下拉列表選擇 {{selected}}</p>
        <select v-model="selected">
            <option disabled value="">請選擇</option>
            <option>A</option>
            <option>B</option>
            <option>C</option>
        </select>

        <p>下拉列表選擇(多選) {{selectedList}}</p>
        <select v-model="selectedList" multiple>
            <option disabled value="">請選擇</option>
            <option>A</option>
            <option>B</option>
            <option>C</option>
        </select>
    </div>
</template>

<script>
export default {
    data() {
        return {
            name: '張三',
            age: 18,
            desc: '自我介紹',

            checked: true,
            checkedNames: [],

            gender: 'male',

            selected: '',
            selectedList: []
        }
    }
}
</script>
  1. 對於 vue 組件的使用,props$emit,組件間通訊,自定義事件,組件生命週期,代碼如下所示:
  • index.vue 如下:
<template>
    <div>
        <Input @add="addHandler"/>
        <List :list="list" @delete="deleteHandler"/>
    </div>
</template>

<script>
import Input from './Input'
import List from './List'

export default {
    components: {
        Input,
        List
    },
    data() {
        return {
            list: [
                {
                    id: 'id-1',
                    title: '標題1'
                },
                {
                    id: 'id-2',
                    title: '標題2'
                }
            ]
        }
    },
    methods: {
        addHandler(title) {
            this.list.push({
                id: `id-${Date.now()}`,
                title
            })
        },
        deleteHandler(id) {
            this.list = this.list.filter(item => item.id !== id)
        }
    },
    created() {
        // eslint-disable-next-line
        console.log('index created')
    },
    mounted() {
        // eslint-disable-next-line
        console.log('index mounted')
    },
    beforeUpdate() {
        // eslint-disable-next-line
        console.log('index before update')
    },
    updated() {
        // eslint-disable-next-line
        console.log('index updated')
    },
}
</script>
  • Input.vue 如下:
<template>
    <div>
        <input type="text" v-model="title"/>
        <button @click="addTitle">add</button>
    </div>
</template>

<script>
import event from './event'

export default {
    data() {
        return {
            title: ''
        }
    },
    methods: {
        addTitle() {
            // 調用父組件的事件
            this.$emit('add', this.title)

            // 調用自定義事件
            event.$emit('onAddTitle', this.title)

            this.title = ''
        }
    }
}
</script>
  • List.vue 如下:
<template>
    <div>
        <ul>
            <li v-for="item in list" :key="item.id">
                {{item.title}}

                <button @click="deleteItem(item.id)">刪除</button>
            </li>
        </ul>
    </div>
</template>

<script>
import event from './event'

export default {
    // props: ['list']
    props: {
        // prop 類型和默認值
        list: {
            type: Array,
            default() {
                return []
            }
        }
    },
    data() {
        return {

        }
    },
    methods: {
        deleteItem(id) {
            this.$emit('delete', id)
        },
        addTitleHandler(title) {
            // eslint-disable-next-line
            console.log('on add title', title)
        }
    },
    created() {
        // eslint-disable-next-line
        console.log('list created')
    },
    mounted() {
        // eslint-disable-next-line
        console.log('list mounted')

        // 綁定自定義事件
        event.$on('onAddTitle', this.addTitleHandler)
    },
    beforeUpdate() {
        // eslint-disable-next-line
        console.log('list before update')
    },
    updated() {
        // eslint-disable-next-line
        console.log('list updated')
    },
    beforeDestroy() {
        // 及時銷燬,否則可能造成內存泄露
        event.$off('onAddTitle', this.addTitleHandler)
    }
}
</script>
  • event.js 如下:
import Vue from 'vue'

export default new Vue()

  1. Vue 高級特性,自定義 v-model、$nextTick、slot、動態和異步組件、keep-alive、mixin,如下所示:
  • 自定義 v-model,代碼如下所示:
<template>
    <!-- 例如:vue 顏色選擇 -->
    <input type="text"
        :value="text1"
        @input="$emit('change1', $event.target.value)"
    >
    <!--
        1. 上面的 input 使用了 :value 而不是 v-model
        2. 上面的 change1 和 model.event1 要對應起來
        3. text1 屬性對應起來
    -->
</template>

<script>
export default {
    model: {
        prop: 'text1', // 對應 props text1
        event: 'change1'
    },
    props: {
        text1: String,
        default() {
            return ''
        }
    }
}
</script>
  • $nextTickVue 是異步渲染,data 改變之後,DOM 不會立刻渲染,$nextTick 會在 DOM 渲染之後被觸發,以獲取最新的 DOM 節點,代碼如下所示:
<template>
  <div id="app">
    <ul ref="ul1">
        <li v-for="(item, index) in list" :key="index">
            {{item}}
        </li>
    </ul>
    <button @click="addItem">添加一項</button>
  </div>
</template>

<script>
export default {
  name: 'app',
  data() {
      return {
        list: ['a', 'b', 'c']
      }
  },
  methods: {
    addItem() {
        this.list.push(`${Date.now()}`)
        this.list.push(`${Date.now()}`)
        this.list.push(`${Date.now()}`)

        // 1. 異步渲染,$nextTick 待 DOM 渲染完再回調
        // 2. 頁面渲染時會將 data 的修改做整合,多次 data 修改只會渲染一次
        this.$nextTick(() => {
          // 獲取 DOM 元素
          const ulElem = this.$refs.ul1
          // eslint-disable-next-line
          console.log( ulElem.childNodes.length )
        })
    }
  }
}
</script>


  • slot,插槽的基本使用,作用域插槽,具名插槽,代碼如下所示:

    • slot.vue,如下
    <template>
        <a :href="url">
            <slot>
                默認內容,即父組件沒設置內容時,這裏顯示
            </slot>
        </a>
    </template>
    
    <script>
    export default {
        props: ['url'],
        data() {
            return {}
        }
    }
    </script>
    
    • scopedSlot.vue,如下
    <template>
        <a :href="url">
            <slot :slotData="website">
                {{website.subTitle}} <!-- 默認值顯示 subTitle ,即父組件不傳內容時 -->
            </slot>
        </a>
    </template>
    
    <script>
    export default {
        props: ['url'],
        data() {
            return {
                website: {
                    url: 'http://wangEditor.com/',
                    title: 'wangEditor',
                    subTitle: '輕量級富文本編輯器'
                }
            }
        }
    }
    </script>
    
  • 動態組件,:is = "component-name" 用法,需要根據數據,動態渲染的常見,即組件類型不確定。異步組件,import() 函數,按需加載,異步加載大組件,代碼如下所示:

<template>
    <div>
        <p>vue 高級特性</p>
        <hr>

        <!-- 自定義 v-model -->
        <!-- <p>{{name}}</p>
        <CustomVModel v-model="name"/> -->

        <!-- nextTick -->
        <!-- <NextTick/> -->

        <!-- slot -->
        <!-- <SlotDemo :url="website.url">
            {{website.title}}
        </SlotDemo> -->
        <!-- <ScopedSlotDemo :url="website.url">
            <template v-slot="slotProps">
                {{slotProps.slotData.title}}
            </template>
        </ScopedSlotDemo> -->

        <!-- 動態組件 -->
        <!-- <component :is="NextTickName"/> -->
        
        <!-- 異步組件 -->
        <!-- <FormDemo v-if="showFormDemo"/>
        <button @click="showFormDemo = true">show form demo</button> -->

        <!-- keep-alive -->
        <!-- <KeepAlive/> -->

        <!-- mixin -->
        <MixinDemo/>
    </div>
</template>

<script>
// import CustomVModel from './CustomVModel'
// import NextTick from './NextTick'
// import SlotDemo from './SlotDemo'
// import ScopedSlotDemo from './ScopedSlotDemo'
// import KeepAlive from './KeepAlive'
import MixinDemo from './MixinDemo'

export default {
    components: {
        // CustomVModel
        // NextTick
        // SlotDemo,
        // ScopedSlotDemo,
        // FormDemo: () => import('../BaseUse/FormDemo'),
        // KeepAlive
        MixinDemo
    },
    data() {
        return {
            name: '張三',
            website: {
                url: 'http://baidu.com/',
                title: '百度',
                subTitle: '百度前端'
            },
            // NextTickName: "NextTick",
            showFormDemo: false
        }
    }
}
</script>
  • keep-alive,緩存組件,頻繁切換,不需要重複渲染,Vue 的常見性能優化,代碼如下所示:

    • keepAlive.vue
          <template>
          <div>
              <button @click="changeState('A')">A</button>
              <button @click="changeState('B')">B</button>
              <button @click="changeState('C')">C</button>
      
              <keep-alive> <!-- tab 切換 -->
                  <KeepAliveStageA v-if="state === 'A'"/> <!-- v-show -->
                  <KeepAliveStageB v-if="state === 'B'"/>
                  <KeepAliveStageC v-if="state === 'C'"/>
              </keep-alive>
          </div>
      </template>
      
      <script>
      import KeepAliveStageA from './KeepAliveStateA'
      import KeepAliveStageB from './KeepAliveStateB'
      import KeepAliveStageC from './KeepAliveStateC'
      
      export default {
          components: {
              KeepAliveStageA,
              KeepAliveStageB,
              KeepAliveStageC
          },
          data() {
              return {
                  state: 'A'
              }
          },
          methods: {
              changeState(state) {
                  this.state = state
              }
          }
      }
      </script>
      
    • keepAliveStateA.vue
      <template>
          <p>state A</p>
      </template>
      
      <script>
      export default {
          mounted() {
              // eslint-disable-next-line
              console.log('A mounted')
          },
          destroyed() {
              // eslint-disable-next-line
              console.log('A destroyed')
          }
      }
      </script>
      
    • keepAliveStateB.vue
        <template>
            <p>state B</p>
        </template>
        
        <script>
        export default {
            mounted() {
                // eslint-disable-next-line
                console.log('B mounted')
            },
            destroyed() {
                // eslint-disable-next-line
                console.log('B destroyed')
            }
        }
        </script>
      
    • keepAliveStateC.vue
      <template>
          <p>state C</p>
      </template>
      
      <script>
      export default {
          mounted() {
              // eslint-disable-next-line
              console.log('C mounted')
          },
          destroyed() {
              // eslint-disable-next-line
              console.log('C destroyed')
          }
      }
      </script>
      
  • mixin,多個組件有相同的邏輯,抽離出來。mixin 並不是完美的解決方案,會有一些問題,Vue3 提出的 Composition API 旨在解決這些問題。同樣的,mixin 也會存在一些問題,變量來源不明確,不利於閱讀,多 mixin 可能會造成命名衝突,mixin 和組件可能出現多對多的關係,複雜度較高,代碼如下所示:

    • Mixin.vue
     <template>
        <div>
            <p>{{name}} {{major}} {{city}}</p>
            <button @click="showName">顯示姓名</button>
        </div>
    </template>
    
    <script>
    import myMixin from './mixin'
    
    export default {
        mixins: [myMixin], // 可以添加多個,會自動合併起來
        data() {
            return {
                name: '張三',
                major: 'web 前端'
            }
        },
        methods: {
        },
        mounted() {
            // eslint-disable-next-line
            console.log('component mounted', this.name)
        }
    }
    </script>
    
    • mixin.js
      export default {
          data() {
              return {
                  city: '上海'
              }
          },
          methods: {
              showName() {
                  // eslint-disable-next-line
                  console.log(this.name)
              }
          },
          mounted() {
              // eslint-disable-next-line
              console.log('mixin mounted', this.name)
          }
      }
      
      
  1. 對於用於 Vue 組件,dispatch、commit、mapState、mapGetters、mapActions 和 mapMutations 也需要熟系。
  2. 對於 vue-router,需要熟系路由模式,hashH5 history,路由配置,動態路由和懶加載。對於 vue-router 路由模式,hash 模式是默認的,如 http://abc.com/#/user/10。H5 history 模式,如 http://abc.com/user/20,後者需要 server 端支持,因此無特殊需求可選擇前者。

二、 Vue 的原理

  1. 對於 vue 的原理,從組件化、響應式、vdomdiff、模版編譯、渲染過程、前端路由幾個方面。
  2. 組件化,很久之前就已經存在了,在 asp、jsp、php、nodejs 都有類似的組件化。數據驅動視圖,MVVM、setState。傳統組件,只是靜態渲染,更新依賴於操作 DOM,數據驅動視圖,vue MVVM,數據驅動視圖,react setState
  3. vue 的響應式,組件 data 的數據一旦變化,立即觸發視圖的更新,實現數據驅動視圖的第一步。vue 的響應式,核心 APIObject.defineProperty。對於 Object.defineProperty 也存在一些缺點,Vue3.0啓用 ProxyProxy 也存在兼容性的問題,兼容性不好,無法 polyfillObject.defineProperty 實現響應式,監聽對象,監聽數組,複雜對象,深度監聽。Object.defineProperty 的缺點,深度監聽,需要遞歸到底,一次性計算量大,無法監聽新增屬性和刪除屬性,Vue.setVue.delete,無法原生監聽數組,需要特殊處理,代碼如下所示:
// 觸發更新視圖
function updateView() {
    console.log('視圖更新')
}

// 重新定義數組原型
const oldArrayProperty = Array.prototype
// 創建新對象,原型指向 oldArrayProperty ,再擴展新的方法不會影響原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function () {
        updateView() // 觸發視圖更新
        oldArrayProperty[methodName].call(this, ...arguments)
        // Array.prototype.push.call(this, ...arguments)
    }
})

// 重新定義屬性,監聽起來
function defineReactive(target, key, value) {
    // 深度監聽
    observer(value)

    // 核心 API
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                // 深度監聽
                observer(newValue)

                // 設置新值
                // 注意,value 一直在閉包中,此處設置完之後,再 get 時也是會獲取最新的值
                value = newValue

                // 觸發更新視圖
                updateView()
            }
        }
    })
}

// 監聽對象屬性
function observer(target) {
    if (typeof target !== 'object' || target === null) {
        // 不是對象或數組
        return target
    }

    // 污染全局的 Array 原型
    // Array.prototype.push = function () {
    //     updateView()
    //     ...
    // }

    if (Array.isArray(target)) {
        target.__proto__ = arrProto
    }

    // 重新定義各個屬性(for in 也可以遍歷數組)
    for (let key in target) {
        defineReactive(target, key, target[key])
    }
}

// 準備數據
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        address: '北京' // 需要深度監聽
    },
    nums: [10, 20, 30]
}

// 監聽數據
observer(data)

// 測試
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增屬性,監聽不到 —— 所以有 Vue.set
// delete data.name // 刪除屬性,監聽不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度監聽
data.nums.push(4) // 監聽數組

  1. 虛擬 DOM( Virtual DOM)diffvdom 是實現 vuereact 的重要基石,diff 算法是 vdom 中最核心、最關鍵的部分,如下所示:
  • DOM 操作非常耗費性能,以前用 jQuery,可以自行控制 DOM 操作的時機,手動調整。VueReact 是數據驅動視圖,如何有效的控制 DOM 操作。
  • 解決方案就可以用 vdom,有了一定複雜度,想減少計算次數比較難。將計算轉移爲 JS 計算,JS 執行速度很快。vdomJS 模擬 DOM 結構,計算出最小的變更,操作 DOM
  • 通過 snabbdom 學習 vdom,簡潔強大的 vdom 庫,易學易用,Vue 參考它實現的 vdomdiff,地址爲 https://github.com/snabbdom/snabbdom。在 Vue3.0 重寫了 vdom 的代碼,優化了性能
  • diff 算法是 vdom 中最核心、最關鍵的部分,在日常使用 vuereact 中可以體現出來,如 key
  • diff 即對比,是一個廣泛的概念,如 linux diff 命令、git diff 等。兩個 JS 對象也可以做 diff,兩棵樹做 diff,如 vdom diff 等等。
  • diff 的時間複雜度 O(n^3),第一,遍歷 tree 1;第二,遍歷 tree2;第三,排序。1000個節點,要計算 1 億次,算法不可用
  • 優化時間複雜度到 O(n),只比較同一層級,不跨級比較。tag 不相同,則直接刪掉重建,不再深度比較。tagkey,兩者都相同,則認爲是相同節點,不再深度比較。
  1. 對於 diff 算法的總結,patchVnode、addVnodes removeVnodes、updateChildrenkey 的重要性。vdom 核心概念很重要,h、vnode、patch、diff、key等等。vdom 存在的價值更加重要,數據驅動視圖,控制 DOM 操作。snabbdom 的簡單實現,代碼如下所示:
const snabbdom = window.snabbdom

// 定義 patch
const patch = snabbdom.init([
    snabbdom_class,
    snabbdom_props,
    snabbdom_style,
    snabbdom_eventlisteners
])

// 定義 h
const h = snabbdom.h

const container = document.getElementById('container')

// 生成 vnode
const vnode = h('ul#list', {}, [
    h('li.item', {}, 'Item 1'),
    h('li.item', {}, 'Item 2')
])
patch(container, vnode)

document.getElementById('btn-change').addEventListener('click', () => {
    // 生成 newVnode
    const newVnode = h('ul#list', {}, [
        h('li.item', {}, 'Item 1'),
        h('li.item', {}, 'Item B'),
        h('li.item', {}, 'Item 3')
    ])
    patch(vnode, newVnode)
})
  1. 模版編譯,模版是 vue 開發中最常用的部分,即與使用相關聯的原理。它不是 html,有指令、插值、JS 表達式,會通過組件渲染和更新過程所去體現出來,如下所示:
  • 前置知識也是 JSwith 語法,能改變 {} 內自由變量的查找方式,vue template complier 將模版編譯爲 render 函數,執行 render 函數生成 vnode
  • with 語法,改變 {} 內自由變量的查找規則,當做 obj 屬性來查找。如果找不到匹配的 obj 屬性,就會報錯。with 要慎用,它打破了作用域規則,易讀性變差
  • 編譯模版,模版不是 html,有指令、插值、JS 表達式,能實現判斷、循環。html 是標籤語言,只有 JS 才能實現判斷、循環,圖靈完備的。因此,模版一定是轉換爲某種 JS 代碼,即編譯模版
  1. 對於 vue 組件中使用 render 代替 template。在有些複雜情況中,不能用 template,可以考慮用 renderReact 一直都用 render,沒有模版,和這裏一樣。所以,vue 組件中使用 render 代替 template,代碼如下所示:
const compiler = require('vue-template-compiler')

// 插值
// // const template = `<p>{{message}}</p>`
// // with(this){return createElement('p',[createTextVNode(toString(message))])}
// // h -> vnode
// // createElement -> vnode

// // // 表達式
// // const template = `<p>{{flag ? message : 'no message found'}}</p>`
// // // with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}

// // // 屬性和動態屬性
// // const template = `
// //     <div id="div1" class="container">
// //         <img :src="imgUrl"/>
// //     </div>
// // `
// // with(this){return _c('div',
// //      {staticClass:"container",attrs:{"id":"div1"}},
// //      [
// //          _c('img',{attrs:{"src":imgUrl}})])}

// // // 條件
// // const template = `
// //     <div>
// //         <p v-if="flag === 'a'">A</p>
// //         <p v-else>B</p>
// //     </div>
// // `
// // with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}

// // 循環
// // const template = `
// //     <ul>
// //         <li v-for="item in list" :key="item.id">{{item.title}}</li>
// //     </ul>
// // `
// // with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}

// // 事件
// // const template = `
// //     <button @click="clickHandler">submit</button>
// // `
// // with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}

// // v-model
// const template = `<input type="text" v-model="name">`
// // 主要看 input 事件
// // with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}

// // render 函數
// // 返回 vnode
// // patch

// // 編譯
// const res = compiler.compile(template)
// console.log(res.render)

// // ---------------分割線--------------

// // // 從 vue 源碼中找到縮寫函數的含義
// // function installRenderHelpers (target) {
// //     target._o = markOnce;
// //     target._n = toNumber;
// //     target._s = toString;
// //     target._l = renderList;
// //     target._t = renderSlot;
// //     target._q = looseEqual;
// //     target._i = looseIndexOf;
// //     target._m = renderStatic;
// //     target._f = resolveFilter;
// //     target._k = checkKeyCodes;
// //     target._b = bindObjectProps;
// //     target._v = createTextVNode;
// //     target._e = createEmptyVNode;
// //     target._u = resolveScopedSlots;
// //     target._g = bindObjectListeners;
// //     target._d = bindDynamicKeys;
// //     target._p = prependModifier;
// // }

  1. 對於組件的渲染和更新過程,一個組件渲染到頁面,修改 data 觸發更新,數據驅動視圖,對背後原理和流程需要很熟悉。響應式,監聽 data 屬性 gettersetter,包括數組。模版編譯,模版到 render 函數,再到 vnodevdom,patch(elem, vnode)patch(vnode, newVnode)

  2. 對於初次渲染過程,更新過程和異步渲染,也需要很熟悉,如下所示:

  • 初次渲染過程,解析模版爲 render 函數或者是在開發環境已完成,vue-loader。觸發響應式,監聽 data 屬性 gettersetter。執行 render 函數,生成 vnodepatch( elem, vnode)
  • 對於更新過程,修改 data,觸發 setter,此前 setter 中已被監聽。重新執行 render 函數,生成 newVnode,patch(vnode, newVnode)
  • 對於異步渲染,需要用到 $nextTick,彙總 data 的修改,一次性更新視圖。減少 DOM 操作次數,提高性能。異步渲染,$nextTickDOM 渲染完再回調。頁面渲染時會將 data 的修改做整合,多次 data 修改只會渲染一次。
  1. 對於模版的渲染與隔離過程,需要理清渲染和響應式的關係、渲染和模版編譯的關係、渲染和 vdom 關係。初次渲染過程,更新過程和異步渲染,同樣需要理清。

  2. 前端路由原理,對於 SPA,都需要路由,vue-router 也是 vue 全家桶的標配之一。vue-router 的路由模式、hashH5 history,如下所示:

  • hash 的特點,如下所示:
    • hash 變化會觸發網頁跳轉,即瀏覽器的前進和後退
    • hash 變化不會刷新頁面,SPA 必須的特點
    • hash 永遠不會提交到 server
  • H5 history,用 url 規範的路由,但跳轉時不刷新頁面,history.pushStatewindow.onpopstate
  • 對於兩者的選擇,如下所示:
    • to B 的系統推薦使用 hash,簡單易用,對 url規範不敏感
    • to C 的系統,可以考慮選擇 H5 history,但需要服務端支持
    • 能選擇簡單的,就別用複雜的,要考慮成本和收益
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章