FE.SRC-Vue實戰與原理筆記

實戰 - 插件

form-validate

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.js"></script>
<div id="app">
    <form @submit="validate">
        <input v-model="text">
        <br>
        <input v-model="email">

        <ul v-if="!$v.valid" style="color:red">
            <li v-for="error in $v.errors">
                {{ error }}
            </li>
        </ul>
        <input type="submit" :disabled="!$v.valid">
    </form>
</div>
<script>
const emailRE = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
const validationPlugin = {
    install(Vue) {
        // 全局注入的方法
        Vue.mixin({
            computed: {
                $v() {
                    const rules = this.$options.validations
                    let valid = true
                    let errors = [] 
                    Object.keys(rules || {}).forEach(key => {
                        const rule = rules[key]
                        const value = this[key]
                        const result = rule.validate(value)
                        if(!result) {
                            valid = false
                            errors.push(
                            rule.message(key, value)
                            )
                        }
                    })
                    return {
                        valid,
                        errors
                    }
                }
            }
        })
    }
}
Vue.use(validationPlugin)
new Vue({
  el: '#app',
  data: {
    text: 'foo',
    email: ''
  },
  validations: {
    text: {
      validate: value => value.length >= 5,
      message: (key, value) => `${key} should have a min length of 5, but got ${value.length}`
    },
    email: {
      validate: value => emailRE.test(value),
      message: key => `${key} must be a valid email`
    }
  },
  methods: {
    validate (e) {
      if (!this.$v.valid) {
        e.preventDefault()
        alert('not valid!')
      }
    }
  }
})
</script>

i18n

<script src="../node_modules/vue/dist/vue.js"></script>

<div id="app">
  <h1>{{ $t('welcome-message') }}</h1>
  <button @click="changeLang('en')">English</button>
  <button @click="changeLang('zh')">中文</button>
  <button @click="changeLang('nl')">Dutch</button>
</div>

<script>
const i18nPlugin = {
  install(Vue,locales){
    Vue.prototype.$t=function(id){
      return locales[this.$root.lang][id]
    }
  }
}

Vue.use(i18nPlugin, /* option */ {
  en: { 'welcome-message': 'hello' },
  zh: { 'welcome-message': '你好' },
  nl: { 'welcome-message': 'Hallo' }
})

new Vue({
  el: '#app',
  data: {
    lang: 'en'
  },
  methods: {
    changeLang (lang) {
      this.lang = lang
    }
  }
})
</script>

實戰 - 組件

Vue組件=Vue實例=new Vue(options)html

  • 屬性vue

    • 自定義屬性 props:組件props中聲明的屬性
    • 原生屬性 attrs:沒有聲明的屬性,默認自動掛載到組件根元素上
    • 特殊屬性 class,style:掛載到組件根元素上
  • 事件node

    • 普通事件 @click,@input,@change,@xxx 經過this.$emit('xxx')觸發
    • 修飾符事件 @input.trim @click.stop
  • 插槽react

    • v-slot:xxx
    • v-slot:xxx="props"
    • 相同名稱的插槽替換

動態導入/延遲加載組件

<template>
  <div> 
    <lazy-component />
  </div>
</template>
<script>
const lazyComponent = () => import('Component.vue')
export default {
  components: { lazyComponent }
}
</script>

基於路由拆分git

const routes = [
  { path: /foo', component: () => import('./RouteComponent.vue') }
]

函數式組件

無狀態 、實例、this上下文、生命週期github

臨時變量組件

<TempVar
  :var1="`hello ${name}`"
  :var2="destroyClock ? 'hello vue' : 'hello world'"
>
  <template v-slot="{ var1, var2 }">
    {{ var1 }}
    {{ var2 }}
  </template>
</TempVar>

<script>
export default {
  functional:true,
  render:(h,ctx)=>{
    return ctx.scopedSlots.default && ctx.scopedSlots.default(ctx.props||{})
  }
}
</script>

批量渲染標籤組件

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.js"></script>
<div id="app">
    <function-component :tags="['h1','h2','h3']"></function-component>
</div>
<script>
    // 函數組件的渲染函數還會接收一個額外的 context 參數,爲沒有實例的函數組件提供上下文信息。
    // 這裏咱們用對象解構context
    const FunctionComponent = {
        functional: true, // 標記組件爲 functional
        render (h, {props:{tags}}){
            return h('div', {attrs: {
                class:'function'
            }},
            tags.map((tag, i) => h(tag, i))
            )
        }
    }
    Vue.component('function-component', FunctionComponent)
    new Vue({el: '#app'})
</script>

封裝應用實例化函數

function createApp ({ el, model, view, actions }) {
  const wrappedActions={}
  Object.keys(actions).forEach(key=>{
    const originalAction=actions[key]
    wrappedActions[key]=()=>{
      const nextModel=originalAction(model)
      vm.model=nextModel
    }
  })

  const vm=new Vue({
    el,
    data:{model},
    render(h){
      return view(h,this.model,wrappedActions)
    }
  })
}

createApp({
  el: '#app',
  model: {
    count: 0
  },
  actions: {
    inc: ({ count }) => ({ count: count + 1 }),
    dec: ({ count }) => ({ count: count - 1 })
  },
  view: (h, model, actions) => h('div', { attrs: { id: 'app' }}, [
    model.count, ' ',
    h('button', { on: { click: actions.inc }}, '+'),
    h('button', { on: { click: actions.dec }}, '-')
  ])
})

高階組件

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.js"></script>
<div id="app">
    <smart-avatar username="vuejs" id="hello">
        <div slot="foo">
            這是一個具名插槽
        </div>
    </smart-avatar>
</div>
<script>
    // mock API
    function fetchURL(username, cb) {
        setTimeout(() => {
            cb('https://avatars3.githubusercontent.com/u/6128107?v=4&s=200')
        }, 500)
    }
    const Avatar = {
        props: ['src'],
        template: `<img :src="src" />`
    }
    // 高階組件withAvatarUrl
    function withAvatarUrl(innerComponent, fetchURL) {
        return {
            props: ['username'],
            data() {
                return {
                    url: `http://via.placeholder.com/200*200`
                }
            },
            created() {
                fetchURL(this.username, url => {
                    this.url = url;
                })
            },
            render(h) {
                // console.log(this.$slots.default);
                // console.log(this.$slots.foo);
                return h(innerComponent, {
                    props: {
                        src: this.url,
                        attrs: this.$attrs
                    }
                }, this.$slots.default)
            }
        }
    }
    const SmartAvatar = withAvatarUrl(Avatar, fetchURL);
    new Vue({
        el: '#app',
        components: {SmartAvatar}
    })
</script>

異步組件

const AsyncComp = () => ({
  // 須要加載的組件。應當是一個 Promise
  component: import('./MyComp.vue'),
  // 加載中應當渲染的組件
  loading: LoadingComp,
  // 出錯時渲染的組件
  error: ErrorComp,
  // 渲染加載中組件前的等待時間。默認:200ms。
  delay: 200,
  // 最長等待時間。超出此時間則渲染錯誤組件。默認:Infinity
  timeout: 3000
})
Vue.component('async-example', AsyncComp)

實戰 - 組件通訊

父傳子props

<!-- 子組件 -->
<template> 
    <ul>
        <li v-for="item in dataList">{{item}}</li>
    </ul> 
</template>

<script>
    export default { 
        props : { dataList : [] }
    }
</script>
<!-- 父組件 -->
<template>
    <component-child v-bind:data-list="dataList"> </component-child> 
    <input v-model="dataInput" v-on:keyup.13="addDataItem()" ></input>
</template>

<script>

import ComponentChild from './child.vue'
export default { 
    data () { 
        return { 
            dataInput: "", 
            dataList : [ 'hello world!','welcome to use vue.js' ] 
        } 
    }, 
    components : { ComponentChild }, 
    methods : { 
        addDataItem () { 
            let self = this 
            if( !(self.dataInput && self.dataInput.length > 0) ) { return } 
            self.dataList.push( self.dataInput ) 
            self.dataInput = "" 
        } 
    }
}
</script>

子傳父組件$emit, $on, $off

在組件中,可使用 $emit, $on, $off 分別來分發、監聽、取消監聽事件面試

// NewTodoInput ---------------------
// ...
methods: {
  addTodo: function () {
    eventHub.$emit('add-todo', { text: this.newTodoText })
    this.newTodoText = ''
  }
}
// DeleteTodoButton ---------------------
// ...
methods: {
  deleteTodo: function (id) {
    eventHub.$emit('delete-todo', id)
  }
}
// Todos ---------------------
// ...
created: function () {
  eventHub.$on('add-todo', this.addTodo)
  eventHub.$on('delete-todo', this.deleteTodo)
},
// 最好在組件銷燬前
// 清除事件監聽
beforeDestroy: function () {
  eventHub.$off('add-todo', this.addTodo)
  eventHub.$off('delete-todo', this.deleteTodo)
},
methods: {
  addTodo: function (newTodo) {
    this.todos.push(newTodo)
  },
  deleteTodo: function (todoId) {
    this.todos = this.todos.filter(function (todo) {
      return todo.id !== todoId
    })
  }
}

內置$parent、$children、$ref;$attrs和$listeners

$ref ref="xxx"
$parent / $children:訪問父 / 子實例正則表達式

<input
    :value="value"
    v-bind="$attrs"
    v-on="listeners"
>
 
<script>
computed: {
  listeners() {
    return {
        ...this.$listeners,
        input: event => 
        this.$emit('input', event.target.value)
      }
  }
}
</script>

高階插件/組件庫 provide & inject(observable)

// 父級組件提供 'foo'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// 子組件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}
// 父級組件提供 'state'
var Provider = {
  provide: {
    state = Vue.observable({ count: 0 })
  },
  // ...
}

// 子組件注入 'foo'
var Child = {
  inject: ['state'],
  created () {
    console.log(this.state) // => { count: 0 }
  }
  // ...
}

全局對象 Event Bus

const state={count:0}

const Counter = {
  data(){return state},
  render:h=>h('div',state.count)
}

new Vue({
  el: '#app',
  components:{Counter},
  methods:{
    inc(){
      state.count++
    }
  }
})
//中央事件總線
var bus = new Vue();
var app = new Vue({
  el: "#app",
  template: `
            <div>
                <brother1></brother1>
                <brother2></brother2>
            </div>
        `
});
// 在組件 brother1 的 methods 方法中觸發事件
bus.$emit("say-hello", "world");
// 在組件 brother2 的 created 鉤子函數中監聽事件
bus.$on("say-hello", function(arg) {
  console.log("hello " + arg);
  // hello world
});

自實現boradcast和dispatch

**$dispatch 和 $broadcast 已經被棄用,使用
Vuex代替**vuex

如下自實現參考 iview/emitter.js at 2.0 · iview/iview -githubnpm

function broadcast(componentName, eventName, params) {
    this.$children.forEach(child => {
        const name = child.$options.name;

        if (name === componentName) {
            child.$emit.apply(child, [eventName].concat(params));
        } else {
            // todo 若是 params 是空數組,接收到的會是 undefined
            broadcast.apply(child, [componentName, eventName].concat([params]));
        }
    });
}
export default {
    methods: {
        dispatch(componentName, eventName, params) {
            let parent = this.$parent || this.$root;
            let name = parent.$options.name;

            while (parent && (!name || name !== componentName)) {
                parent = parent.$parent;

                if (parent) {
                    name = parent.$options.name;
                }
            }
            if (parent) {
                parent.$emit.apply(parent, [eventName].concat(params));
            }
        },
        broadcast(componentName, eventName, params) {
            broadcast.call(this, componentName, eventName, params);
        }
    }
};

原理 - 響應式

單向數據流,雙向綁定語法糖

demo

<PersonalInfo
  v-model="phoneInfo"
  :zip-code.sync="zipCode"
/>
<!-- 等同於 -->
<PersonalInfo
  :phone-info="phoneInfo"
  @change="val=>(phoneInfo=val)"
  :zip-code="zipCode"
  @update:zipCode="val=>(zipCode=val)"
/>

原理

數據劫持+觀察訂閱模式:

  • 在讀取屬性的時候依賴收集,在改變屬性值的時候觸發依賴更新
  • 實現一個observer,劫持對象屬性
  • 實現一個全局的訂閱器Dep,能夠追加訂閱者,和通知依賴更新
  • 在讀取屬性的時候追加當前依賴到Dep中,在設置屬性的時候循環觸發依賴的更新
  • new Vue(options)建立實例的時候,initData()進行數據劫持
  • 經過Object.defineProperty(obj,key,desc)對data進行數據劫持,即建立get/set函數

    • 這裏須要考慮對對象的以及對數組的數據劫持(支持 push,pop,shift,unshift,splice,sort,reverse,不支持 filter,concat,slice)
    • 對象遞歸調用
    • 數組變異方法的解決辦法:代理原型/實例方法
    • 避免依賴重讀Observer
// 全局的依賴收集器Dep
   window.Dep = class Dep {
       constructor() {this.subscribers = new Set()}
       depend() {activeUpdate&&this.subscribers.add(activeUpdate)}
       notify() {this.subscribers.forEach(sub =>sub())}
   }
   let activeUpdate
   function autorun(update) {
       function wrapperUpdate() {
           activeUpdate = wrapperUpdate
           update()
           activeUpdate = null
       }
       wrapperUpdate()
   }
   function observer(obj) {
       Object.keys(obj).forEach(key => {
           var dep = new Dep()
           let internalValue = obj[key]
           Object.defineProperty(obj, key, {
               get() {
                   // console.log(`getting key "${key}": ${internalValue}`)
                   // 將當前正在運行的更新函數追加進訂閱者列表
                   activeUpdate&&dep.depend() //收集依賴
                   return internalValue
               },
               set(newVal) {
                //console.log(`setting key "${key}" to: ${internalValue}`)
                // 加個if判斷,數據發生變化再觸發更新
                if(internalValue !== newVal) {
                       internalValue = newVal
                       dep.notify() // 觸發依賴的更新
                }
               }
           })
       })
   }
   let state = {count:0}
   observer(state);
   autorun(() => {
       console.log('state.count發生變化了', state.count)
   })
   state.count = state.count + 5;
   // state.count發生變化了 0
   // state.count發生變化了 5

MVVM實現

實現mvvm-github

Model-View-ViewModel,其核心是提供對View 和 ViewModel 的雙向數據綁定,這使得ViewModel 的狀態改變能夠自動傳遞給 View

observer

  • proxy 方法遍歷 data 的 key,把 data 上的屬性代理到 vm 實例上(經過Object.defineProperty 的 getter 和 setter )
  • observe(data, this) 給 data 對象添加 Observer作監聽。

    • 建立一個 Observer 對象

      • 建立了一個 Dep 對象實例(觀察者模式)

        • 遍歷data,convert(defineReactive) 方法使他們有getter、setter
        • getter 和 setter 方法調用時會分別調用 dep.depend 方法和 dep.notify

          • depend:把當前 Dep 的實例添加到當前正在計算的Watcher 的依賴中
          • notify:遍歷了全部的訂閱 Watcher,調用它們的 update 方法

computed

  • computed初始化被遍歷computed,使用Object.defineProperty進行處理,依賴收集到Dep.target
  • 觸發data值時會觸發Watcher監聽函數
  • computed值緩存 watcher.dirty決定了計算屬性值是否須要從新計算,默認值爲true,即第一次時會調用一次。每次執行以後watcher.dirty會設置爲false,只有依賴的data值改變時纔會觸發

mixin

全局註冊的選項,被引用到你的每一個組件中
一、Vue.component 註冊的 【全局組件】
二、Vue.filter 註冊的 【全局過濾器】
三、Vue.directive 註冊的 【全局指令】
四、Vue.mixin 註冊的 【全局mixin】

合併權重
一、組件選項
二、組件 - mixin
三、組件 - mixin - mixin
四、.....
x、全局 選項
函數合併疊加(data,provide)
數組疊加(created,watch)
原型疊加(components,filters,directives)
兩個對象合併的時候,不會相互覆蓋,而是 權重小的 被放到 權重大 的 的原型上
覆蓋疊加(props,methods,computed,inject)
兩個對象合併,若是有重複key,權重大的覆蓋權重小的
直接替換(el,template,propData 等)

filter

something | myFilter 被解析成_f('myFilter')( something )

nextTick

Vue.js 在默認狀況下,每次觸發某個數據的 setter 方法後,對應的 Watcher 對象其實會被 push 進一個隊列 queue 中,在下一個 tick 的時候將這個隊列 queue 所有拿出來 run( Watcher 對象的一個方法,用來觸發 patch 操做) 一遍。

原理 - virtaul DOM

真實DOM操做昂貴,虛擬DOM就是js對象,操做代價小

模板編譯&渲染

平時開發寫vue文件都是用模板template的方法寫html,模板會被編譯成render函數,流程以下:

  • 初始化的時候

    - 模板會被編譯成render函數
    - render函數返回虛擬DOM
    - 生成真正的DOM
  • 數據更新的時候

    - render函數返回新的virtual Dom
    - 新的virtual Dom和舊的virtual Dom作diff
    - 將差別運用到真實DOM
  • render API

    //template, jsx, render本質都是同樣的, 都是一種dom和數據狀態之間關係的表示
        render(h) {
            h(tag, data, children)
        }
        // tag能夠是原生的html標籤
        render(h) {
            return h('div', { attrs: {}}, [])
        }
        // 也能夠是一個vue component
        import vueComponent from '...'
        render(h) {
            h(vueComponent, {
                props: {} 
            })
        }
    • 偏邏輯用render,偏視圖用template

compile

compile 編譯能夠分紅 parse、 optimize 與 generate 三個階段,最終須要獲得 render function。

  • parse:會用正則等方式解析 template 模板中的指令、class、style 等數據,造成 AST。

    • transclude(el, option) 把 template 編譯成一段 document fragment
    • compileNode(el, options) 深度遍歷DOM,正則解析指令
    • vm.bindDir(descriptor, node, host, scope) 根據 descriptor 實例化不一樣的 Directive 對象
    • Directive 在初始化時經過 extend(this, def) 擴展 bind 和 update,建立了 Watcher關聯update
    • 解析模板字符串生成 AST `const ast = parse(template.trim(), options)
      循環解析 template,利用正則表達式順序解析模板,當解析到開始標籤、閉合標籤、文本的時候都會分別執行對應的回調函數,來達到構造 AST 樹的目的。 AST 元素節點總共有 3 種類型,type 爲 1 表示是普通元素,爲 2 表示是表達式,爲 3 表示是純文本。
  • optimize:標記 static 靜態節點,而減小了比較的過程 等優化

    • 優化語法樹 optimize(ast, options)
      深度遍歷這個 AST 樹,去檢測它的每一顆子樹是否是靜態節點,若是是靜態節點則它們生成 DOM 永遠不須要改變(標記靜態節點 markStatic(root);標記靜態根 markStaticRoots(root, false))
  • generate:是將 AST 轉化成 render function 字符串

    • AST轉可執行的代碼 const code = generate(ast, options)
    • vue模板編譯先後:

      `<ul :class="bindCls" class="list" v-if="isShow">
          <li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
      </ul>`
      
      
      with(this){
        return (isShow) ?
          _c('ul', {
              staticClass: "list",
              class: bindCls
            },
            _l((data), function(item, index) {
              return _c('li', {
                on: {
                  "click": function($event) {
                    clickItem(index)
                  }
                }
              },
              [_v(_s(item) + ":" + _s(index))])
            })
          ) : _e()
      }

      對比react的jsx編譯先後

      `<div id="1" class="li-1"> 
          Hello World
          <MyComp></MyComp>
      </div>`
      
      h('div',{ id: '1', 'class': 'li-1' },'Hello World',
          h(MyComp, null)
      )

diff

  • vnode

    {
      el:  div  //對真實的節點的引用,本例中就是document.querySelector('#id.classA')
      tagName: 'DIV',   //節點的標籤
      sel: 'div#v.classA'  //節點的選擇器
      data: null,       // 一個存儲節點屬性的對象,對應節點的el[prop]屬性,例如onclick , style
      children: [], //存儲子節點的數組,每一個子節點也是vnode結構
      text: null,    //若是是文本節點,對應文本節點的textContent,不然爲null
    }
  • 核心 patch (oldVnode, vnode)

    • key和sel相同纔去比較,不然新替舊
    • patchVnode (oldVnode, vnode)節點比較5種狀況

      • if (oldVnode === vnode),他們的引用一致,能夠認爲沒有變化
      • if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text),文本節點的比較,須要修改,則會調用Node.textContent = vnode.text
      • if( oldCh && ch && oldCh !== ch ), 兩個節點都有子節點,並且它們不同,這樣咱們會調用updateChildren函數比較子節點
      • else if (ch),只有新的節點有子節點,調用createEle(vnode),vnode.el已經引用了老的dom節點,createEle函數會在老dom節點上添加子節點
      • else if (oldCh),新節點沒有子節點,老節點有子節點,直接刪除老節點
  • 同層比較做用:將一棵樹轉換成另外一棵樹的最小操做次數是O(n^3),同層是O(1)
  • key的做用:

    • 爲了在數據變化時強制更新組件,以免「原地複用」帶來的反作用。
    • 在交叉對比沒有結果(列表數據的從新排序,插,刪)的時候會採用key來提升這個diff速度(不設key,newCh和oldCh只會進行頭尾兩端的相互比較,設key後,除了頭尾兩端的比較外,還會從用key生成的對象oldKeyToIdx中查找匹配的節點,從而移動dom而不是銷燬再建立)

vue&react vdom區別

Vue 很「 囂張 」,它宣稱能夠更快地計算出Virtual DOM的差別,這是因爲它在渲染過程當中,因爲vue會跟蹤每個組件的依賴收集,經過setter / getter 以及一些函數的劫持,可以精確地知道變化,並在編譯過程標記了static靜態節點,在接下來新的Virtual DOM 而且和原來舊的 Virtual DOM進行比較時候,跳過static靜態節點。因此不須要從新渲染整個組件樹。

React默認是經過比較引用的方式進行,當某個組件的狀態發生變化時,它會以該組件爲根,從新渲染整個組件子樹。若是想避免沒必要要的子組件從新渲染,你須要在全部可能的地方使用PureComponent,或者手動實現shouldComponentUpdate方法。可是Vue中,你能夠認定它是默認的優化。

摘自 https://juejin.im/post/5b6178...

vdom實現

類vue vdom

snabbdom-github

snabbdom源碼閱讀分析

Vue 2.0 的 virtual-dom 實現簡析

類react vdom

preact-github

preact工做原理

原理 - Router(路由)

vue插件,經過hash /history 2中方式實現可配路由
Hash

  1. push(): 設置新的路由添加歷史記錄並更新視圖,經常使用狀況是直接點擊切換視圖,調用流程:
  • $router.push() 顯式調用方法
  • HashHistory.push() 根據hash模式調用,設置hash並添加到瀏覽器歷史記錄(window.location.hash= XXX)
  • History.transitionTo() 開始更新
  • History.updateRoute() 更新路由
  • app._route= route
  • vm.render() 更新視圖
  1. replace: 替換當前路由並更新視圖,經常使用狀況是地址欄直接輸入新地址,流程與push基本一致

但流程2變爲替換當前hash (window.location.replace= XXX)
3.監聽地址欄變化:在setupListeners中監聽hash變化(window.onhashchange)並調用replace

History

  1. push:與hash模式相似,只是將window.hash改成history.pushState
  2. replace:與hash模式相似,只是將window.replace改成history.replaceState
  3. 監聽地址變化:在HTML5History的構造函數中監聽popState(window.onpopstate)

實戰

const Foo = {
    props: ['id'],
    template: `<div>foo with id: {{id}} </div>`
}
const Bar = {
    template: `<div>bar</div>`
}
const NotFound = {
    template: `<div>not found</div>`
}
const routeTable = {
    '/foo/:id': Foo,
    '/bar': Bar,
}
const compiledRoutes = [];
Object.keys(routeTable).forEach(path => {
    const dynamicSegments = []
    const regex = pathToRegexp(path, dynamicSegments)
    const component = routeTable[path]
    compiledRoutes.push({
        component,
        regex,
        dynamicSegments
    })
})
window.addEventListener('hashchange', () => {
    app.url = window.location.hash.slice(1);
})
const app = new Vue({
    el: '#app',
    data() {
        return {
            url: window.location.hash.slice(1)
        }
    },
    render(h) {
        const url = '/' + this.url
        let componentToRender
        let props = {}
        compiledRoutes.some(route => {
            const match = route.regex.exec(url)
            if (match) {
                componentToRender = route.component
                route.dynamicSegments.forEach((segment,index) => {
                    props[segment.name] = match[index+1]
                })
            }
        })
            return h('div', [
            h('a', { attrs: { href: '#foo/123' } }, 'foo123'),
            '|',
            h('a', { attrs: { href: '#foo/234' } }, 'foo234'),
            '|',
            h('a', { attrs: { href: '#bar' } }, 'bar'),
            h(componentToRender || NotFound, { props })
        ])
    }
})

原理 - props(屬性)

父組件怎麼傳值給子組件的 props

  • 父組件的模板 會被解析成一個 模板渲染函數,執行時會綁定 父組件爲做用域

    (function() {
        with(this){ 
            return _c('div',{staticClass:"a"},[
                _c('testb',{attrs:{"child-name":parentName}})
            ],1)
        }
    })
  • 因此渲染函數內部全部的變量,都會從父組件對象 上去獲取

組件怎麼讀取 props

  • 子組件拿到父組件賦值事後的 attr,篩選出 props,而後保存到實例的_props 中,並逐一複製到實例上,響應式的。

父組件數據變化,子組件props如何更新
父組件數據變化,觸發set,從而通知依賴收集器的watcher從新渲染

原理 - Vuex

vuex 僅僅是做爲 vue 的一個插件而存在,不像 Redux,MobX 等庫能夠應用於全部框架,vuex 只能使用在 vue 上,很大的程度是由於其高度依賴於 vue 的 computed 依賴檢測系統以及其插件系統,

vuex 總體思想誕生於 flux,可其的實現方式完徹底全的使用了 vue 自身的響應式設計,依賴監聽、依賴收集都屬於 vue 對對象 Property set get 方法的代理劫持。vuex 中的 store 本質就是沒有 template 的隱藏着的 vue 組件;

state

  • this.$store.state.xxx 取值
  • 提供一個響應式數據
  • vuex 就是一個倉庫,倉庫裏放了不少對象。其中 state 就是數據源存放地,對應於通常 vue 對象裏面的 data
  • state 裏面存放的數據是響應式的,vue 組件從 store 讀取數據,如果 store 中的數據發生改變,依賴這相數據的組件也會發生更新
  • 它經過 mapState 把全局的 state 和 getters 映射到當前組件的 computed 計算屬性

getter

  • this.$store.getters.xxx 取值
  • 藉助vue計算屬性computed實現緩存
  • getter 能夠對 state 進行計算操做,它就是 store 的計算屬性

雖然在組件內也能夠作計算屬性,可是 getters 能夠在多給件之間複用

  • 若是一個狀態只在一個組件內使用,是能夠不用 getters

mutaion

  • this.$store.commit("xxx") 賦值
  • 更改state方法
  • action 相似於 muation, 不一樣在於:action 提交的是 mutation,而不是直接變動狀態
  • action 能夠包含任意異步操做

action

  • this.$store.dispatch("xxx") 賦值
  • 觸發mutation方法

module

  • Vue.set動態添加state到響應式數據中

簡單版Vuex實現

//min-vuex
import Vue from 'vue'
const Store = function Store (options = {}) {
  const {state = {}, mutations={}} = options
  this._vm = new Vue({
    data: {
      $$state: state
    },
  })
  this._mutations = mutations
}
Store.prototype.commit = function(type, payload){
  if(this._mutations[type]) {
    this._mutations[type](this.state, payload)
  }
}
Object.defineProperties(Store.prototype, { 
  state: { 
    get: function(){
      return this._vm._data.$$state
    }
  }
});
export default {Store}

使用 Vuex 只需執行 Vue.use(Vuex),並在 Vue 的配置中傳入一個 store 對象的示例,store 是如何實現注入的?

Vue.use(Vuex) 方法執行的是 install 方法,它實現了 Vue 實例對象的 init 方法封裝和注入,使傳入的 store 對象被設置到 Vue 上下文環境的store中。所以在VueComponent任意地方都可以經過this.store 訪問到該 store。

state 內部支持模塊配置和模塊嵌套,如何實現的?

在 store 構造方法中有 makeLocalContext 方法,全部 module 都會有一個 local context,根據配置時的 path 進行匹配。因此執行如 dispatch('submitOrder', payload)這類 action 時,默認的拿到都是 module 的 local state,若是要訪問最外層或者是其餘 module 的 state,只能從 rootState 按照 path 路徑逐步進行訪問。

在執行 dispatch 觸發 action(commit 同理)的時候,只需傳入(type, payload),action 執行函數中第一個參數 store 從哪裏獲取的?

store 初始化時,全部配置的 action 和 mutation 以及 getters 均被封裝過。在執行如 dispatch('submitOrder', payload)的時候,actions 中 type 爲 submitOrder 的全部處理方法都是被封裝後的,其第一個參數爲當前的 store 對象,因此可以獲取到 { dispatch, commit, state, rootState } 等數據。

Vuex 如何區分 state 是外部直接修改,仍是經過 mutation 方法修改的?

Vuex 中修改 state 的惟一渠道就是執行 commit('xx', payload) 方法,其底層經過執行 this._withCommit(fn) 設置_committing 標誌變量爲 true,而後才能修改 state,修改完畢還須要還原_committing 變量。外部修改雖然可以直接修改 state,可是並無修改_committing 標誌位,因此只要 watch 一下 state,state change 時判斷是否_committing 值爲 true,便可判斷修改的合法性。

調試時的"時空穿梭"功能是如何實現的?

devtoolPlugin 中提供了此功能。由於 dev 模式下全部的 state change 都會被記錄下來,'時空穿梭' 功能其實就是將當前的 state 替換爲記錄中某個時刻的 state 狀態,利用 store.replaceState(targetState) 方法將執行 this._vm.state = state 實現。

原理 - SSR

Vue.js 是構建客戶端應用程序的框架。默認狀況下,能夠在瀏覽器中輸出 Vue 組件,進行生成 DOM 和操做 DOM。

然而,也能夠將同一個組件渲染爲服務器端的 HTML 字符串,將它們直接發送到瀏覽器,最後將靜態標記"混合"爲客戶端上徹底交互的應用程序。

服務器渲染的 Vue.js 應用程序也能夠被認爲是"同構"或"通用",由於應用程序的大部分代碼均可以在服務器和客戶端上運行。

服務端渲染的核心就在於:經過vue-server-renderer插件的renderToString()方法,將Vue實例轉換爲字符串插入到html文件

其餘

vue 企業級應用模板-github

VueConf 2018 杭州(第二屆Vue開發者大會)

Vue 3.0 進展 - 尤雨溪

參考資料

深刻響應式原理 —— Vue.js官網

Advanced Vue.js Features from the Ground Up - 尤雨溪

Vue 源碼解析:深刻響應式原理 - 黃軼

Vue 源碼研究會 - 神仙朱

vue組件之間8種組件通訊方式總結 - zhoulu_hp

Vue問得最多的面試題 - yangcheng

剖析 Vue.js 內部運行機制 - 染陌

相關文章
相關標籤/搜索