乐趣区

FESRCVue实战与原理笔记

实战 – 插件

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)

  • 属性

    • 自定义属性 props: 组件 props 中声明的属性
    • 原生属性 attrs:没有声明的属性,默认自动挂载到组件根元素上
    • 特殊属性 class,style:挂载到组件根元素上
  • 事件

    • 普通事件 @click,@input,@change,@xxx 通过 this.$emit(‘xxx’)触发
    • 修饰符事件 @input.trim @click.stop
  • 插槽

    • v-slot:xxx
    • v-slot:xxx=”props”
    • 相同名称的插槽替换

动态导入 / 延迟加载组件

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

基于路由拆分

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

函数式组件

无状态、实例、this 上下文、生命周期

临时变量组件

<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 代替 **

以下自实现参考 iview/emitter.js at 2.0 · iview/iview -github

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

全局注册的选项,被引用到你的每个组件中
1、Vue.component 注册的【全局组件】
2、Vue.filter 注册的【全局过滤器】
3、Vue.directive 注册的【全局指令】
4、Vue.mixin 注册的【全局 mixin】

合并权重
1、组件选项
2、组件 – mixin
3、组件 – mixin – mixin
4、…..
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 内部运行机制 – 染陌

退出移动版