FE.SRC-Vue实战与原理笔记

栏目: 编程语言 · 发布时间: 4年前

内容简介:Vue组件=Vue实例=new Vue(options)基于路由拆分无状态 、实例、this上下文、生命周期

实战 - 插件

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()
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 内部运行机制 - 染陌


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

RESTful Web Services Cookbook

RESTful Web Services Cookbook

Subbu Allamaraju / Yahoo Press / 2010-3-11 / USD 39.99

While the REST design philosophy has captured the imagination of web and enterprise developers alike, using this approach to develop real web services is no picnic. This cookbook includes more than 10......一起来看看 《RESTful Web Services Cookbook》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

URL 编码/解码
URL 编码/解码

URL 编码/解码