Skip to content

利用 $attrs 实现爷孙之间的通信

在爷组件准备数据

vue
<Father pa="xxx" bb="ddd" aa="jjj"></Father>

在父组件进行原封不动的向下传递

vue
<Er v-bind="$attrs"></Er>
<script>
import Er from '@/components/er.vue'
export default {
  inheritAttrs: false, // 可将从爷组件传递过来的值不在此组件展示
  components: {
    Er
  }
}
</script>

在孙组件中使用

vue
<template>
  <div>
    {{ $attrs.aa }}
  </div>
</template>
<script>
export default {
  created() {
    console.log(this.$attrs) // {pa: "xxx", bb: "ddd", aa: "jjj"}
  }
}
</script>
  • 父组件通过 v-bind 可将从爷组件传递的数据进行展开

可通过 inheritAttrs 属性控制不在父组件展示

自定义饿了么表单

使用

vue
<template>
  <div id="app">
    <KForm :model="model" :rules="rules" ref="loginForm">
      <myForm label="用户名" prop="username">
        <Father v-model="model.username" placeholder="请输入用户名"></Father>
      </myForm>
      <myForm label="密码" prop="password">
        <Father v-model="model.password" placeholder="请输入密码"></Father>
      </myForm>
      <myForm>
        <button @click="login">登录</button>
      </myForm>
    </KForm>
  </div>
</template>

<script>
import Father from './components/father.vue'
import myForm from './components/myForm.vue'
import KForm from './components/KForm.vue'
export default {
  components: {
    Father,
    myForm,
    KForm
  },
  data() {
    return {
      model: {
        username: '',
        password: ''
      },
      rules: {
        username: [{ required: true, message: '请输入用户名' }],
        password: [{ required: true, message: '请输入密码' }]
      }
    }
  },
  methods: {
    login() {
      this.$refs.loginForm.validate(isOk => {
        if (isOk) {
          // 合法
          console.log('发起登录请求')
        } else {
          console.log('不合法')
        }
      })
    }
  }
}
</script>

外层 Form 组件

vue
<template>
  <div>
    <slot></slot>
  </div>
</template>

<script>
export default {
  name: 'KForm',
  provide() {
    return {
      form: this // 这里将表单实例传递给子孙组件,以便子孙组件获取数据(类似 ref)
    }
  },
  props: {
    model: {
      type: Object,
      required: true
    },
    rules: Object
  },
  created() {
    this.fields = []
    this.$on('kkb.form.addField', item => {
      this.fields.push(item)
    })
  },
  methods: {
    validate(cb) {
      // 全局校验方法,方便进行全局校验
      // 执行内部所有FormItem校验方法,统一处理结果
      // 将子组件数组转换为Promise数组
      // const tasks = this.$children
      //   .filter(item => item.prop)
      //   .map(item => item.validate());
      const tasks = this.fields.map(item => item.validate())
      // 统一处理结果
      Promise.all(tasks)
        .then(() => cb(true))
        .catch(() => cb(false))
    }
  }
}
</script>

表单项组件

vue
<template>
  <div>
    <!-- 显示 label -->
    <label v-if="label">{{ label }}</label>
    <!-- 显示内部表单元素 -->
    <slot></slot>
    <p v-if="error" class="error">{{ error }}</p>
    <!-- <p>{{ form.rules[prop] }}</p> 获取表单校验-->
    <!-- <p>{{ form.model[prop] }}</p> form.model[prop] 可以拿到从父组件传递过来的表单值-->
  </div>
</template>

<script>
import Schema from 'async-validator'
import emitter from '@/mixins/emitter'
export default {
  inject: ['form'],
  mixins: [emitter],
  props: {
    label: {
      type: String,
      default: ''
    },
    prop: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      error: ''
    }
  },
  mounted() {
    this.$on('validate', () => {
      this.validate()
    })
    // 派发事件通知 KForm ,新增一个KFormItem 实例
    if (this.prop) {
      this.dispatch('KForm', 'kkb.form.addField', [this])
    }
  },
  methods: {
    // 校验表单项
    validate() {
      // element 使用的是 async-validator 这个插件做校验
      // 获取校验规则和当前数据
      const rules = this.form.rules[this.prop]
      const value = this.form.model[this.prop]
      // 用 async-validator 包校验
      const schema = new Schema({ [this.prop]: rules })
      // 返回 promise 全局可以统一处理
      return schema.validate({ [this.prop]: value }, errors => {
        // 如果 errors 存在说明校验失败
        if (errors) {
          // 显示父组件传递过来的校验失败提示信息
          this.error = errors[0].message
        } else {
          // 校验通过清空错误提示信息
          this.error = ''
        }
      })
    }
  }
}
</script>

<style scope>
.error {
  color: red;
}
</style>

里层组件

vue
<template>
  <div>
    <input :type="type" :value="value" @input="onInput" v-bind="$attrs" />
  </div>
</template>

<script>
import emitter from '@/mixins/emitter'
export default {
  name: 'myForm',
  inheritAttrs: false,
  mixins: [emitter], // 混入广播函数
  props: {
    value: {
      type: String,
      default: ''
    },
    type: {
      type: String,
      default: 'text'
    }
  },
  methods: {
    onInput(e) {
      this.$emit('input', e.target.value)
      // 触发校验规则
      // 这里不能简单的使用 $parent 派发事件,因为用户可能在 myForm 组件内添加其他组件,所以此组件的父组件就不一定是 myForm
      // this.$parent.$emit("validate");
      this.dispatch('myForm', 'validate')
    }
  }
}
</script>

emitter

js
// 广播:自上而下派发事件
// componentName 组件名,eveentName 事件名,params 参数集合
function broadcast(componentName, eveentName, params) {
  // 遍历所有子元素,如果子元素 componentName 和传入的相同则派发事件
  this.$chidren.forEach(child => {
    var name = child.$options.componentName
    if (componentName === name) {
      child.$emit.apply(child, [eveentName].concat(params))
    } else {
      broadcast.apply(child, [componentName, eveentName].concat([params]))
    }
  })
}
export default {
  methods: {
    // 冒泡查找 componentName 相同的组件并派发事件
    dispatch(componentName, eveentName, params) {
      var parent = this.$parent || this.$root
      var name = parent.$options.componentName
      // 向上查找直到找到相同名称的组件
      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent
        if (parent) {
          name = parent.$options.componentName
        }
      }
      // 如果找到了就派发事件
      if (parent) {
        parent.$emit.apply(parent, [eveentName].concat(params))
      }
    },
    broadcast(componentName, eveentName, params) {
      broadcast.call(this, componentName, eveentName, params)
    }
  }
}

弹框组件

vue
<template>
  <div class="box" v-if="isShow">
    <h3>{{ title }}</h3>
    <p class="box-content">{{ message }}</p>
  </div>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      default: ''
    },
    message: {
      type: String,
      default: ''
    },
    duration: {
      type: Number,
      default: 1000
    }
  },
  data() {
    return {
      isShow: false
    }
  },
  methods: {
    show() {
      this.isShow = true
      setTimeout(this.hide, this.duration)
    },
    hide() {
      this.isShow = false
      this.remove()
    }
  }
}
</script>

<style>
.box {
  position: fixed;
  width: 50%;
  top: 16px;
  left: 25%;
  text-align: center;
  pointer-events: none;
  background-color: grey;
  box-sizing: border-box;
}
.box-content {
  width: 200px;
  margin: 10px auto;
  font-size: 14px;
  padding: 8px 16px;
  background-color: red;
  border-radius: 3px;
  margin-bottom: 8px;
}
</style>

created.js

js
import Vue from 'vue'
// 传入一个组件配置
// 创建它的实例,并将它挂载到body上
export default function create(Component, props) {
  // 实例创建
  // Vue.extend() 方案
  const Ctor = Vue.extend(Component)
  // 创建组件实例
  const comp = new Ctor({ propsData: props })
  comp.$mount()
  document.body.appendChild(comp.$el)
  comp.remove = () => {
    document.body.removeChild(comp.$el)
    comp.$destroy()
  }
  // 第二种方案
  // const vm = new Vue({
  //   render(h) {
  //     return h(Component, { props })
  //   }
  // }).$mount() // $mount() 本质上就是将虚拟 dom 转换为真实的 dom

  // 通过 vm.$el 获取生成的dom
  // document.body.appendChild(vm.$el)
  // // 删除函数
  // // 获取组件实例
  // const comp = vm.$children[0]

  // // 未来不需要了需要删除元素,创建一个删除函数
  // comp.remove = () => {
  //   document.body.removeChild(vm.$el)
  //   vm.$destroy()
  // }
  return comp
}

使用

vue
<template>
  <div id="app">
    <KForm :model="model" :rules="rules" ref="loginForm">
      <myForm label="用户名" prop="username">
        <Father v-model="model.username" placeholder="请输入用户名"></Father>
      </myForm>
      <myForm label="密码" prop="password">
        <Father v-model="model.password" placeholder="请输入密码"></Father>
      </myForm>
      <myForm>
        <button @click="login">登录</button>
      </myForm>
    </KForm>
  </div>
</template>

<script>
import Father from './components/father.vue'
import myForm from './components/myForm.vue'
import KForm from './components/KForm.vue'
import create from '@/utils/created'
import Notice from './components/Notice.vue'
export default {
  components: {
    Father,
    myForm,
    KForm
  },
  data() {
    return {
      model: {
        username: '',
        password: ''
      },
      rules: {
        username: [{ required: true, message: '请输入用户名' }],
        password: [{ required: true, message: '请输入密码' }]
      }
    }
  },
  methods: {
    login() {
      this.$refs.loginForm.validate(isOk => {
        // 创建notice实例//////////////////////////
        create(Notice, {
          title: '村长喊你来搬砖',
          message: isOk ? '请求登录' : '校验失败',
          duration: 3000
        }).show()
        //////////////////////////
        if (isOk) {
          // 合法
          console.log('发起登录请求')
        } else {
          console.log('不合法')
        }
      })
    }
  }
}
</script>

vue-router 原理

js
// 1.通过 hashchange 事件监听路由地址变化
// 2.实现两个全局组件 router-link 和 router-view
// 3.router-link 最终会被渲染成 a 标签,用户通过 to 属性传递路由,组件通过这个值给 a 标签 href 属性赋值实现跳转
// 4.router-view 实际上就是一个容器用于渲染当前路由对应组件内容,先获取到路由表信息和当前路由匹配的组件通过render函数渲染对应组件

实现 vue-router

先在 main.js 中引入并注册 KRouter

创建 Krouter 路由表

实现两个全局组件和一个 install 方法(Vue.use 时会执行这个方法)、KVueRouter 类

js
let KVue
// 实现一个 install 方法
class KVueRouter {
  constructor(options) {
    this.$options = options
    // 响应式数据
    const initial = window.location.hash.slice(1) || '/'
    KVue.util.defineReactive(this, 'current', initial) // 监听类似于 Object.defineProperty

    // this.current = '/'
    // 监听事件
    window.addEventListener('hashchange', this.onHashChange.bind(this))
    window.addEventListener('load', this.onHashChange.bind(this))
    // 缓存路由映射关系
    // this.routeMap = {}
    // this.$options.routes.forEach(route => {
    //   this.routeMap[route.path] = route
    // })
  }
  onHashChange() {
    this.current = window.location.hash.slice(1)
  }
}

// 形参是Vue构造函数
KVueRouter.install = function (Vue) {
  // 保存构造函数
  KVue = Vue
  // 挂载$router
  Vue.mixin({
    beforeCreate() {
      // 全局混入,将来在组件实例化的时候才执行
      // 此时 router 实例已经存在
      // 判断是否是根组件
      if (this.$options.Krouter) {
        // 挂载
        Vue.prototype.$router = this.$options.Krouter
      }
    }
  })
  // 实现两个全局组件
  Vue.component('router-link', {
    // 记录跳转地址
    props: {
      to: {
        type: String,
        required: true
      }
    },
    // h 是 createElement 函数
    render(h) {
      // 将来希望输出一个 a 标签,第一个参数是创建的标签名,第二个是内容
      return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default)
      // return h('a', { attrs: { href: '#' + this.to } }, 'link')
    }
  })
  // router-view 其实就是一个容器
  Vue.component('router-view', {
    render(h) {
      // 获取路由器实例
      const routes = this.$router.$options.routes // 获取路由表
      const current = this.$router.current
      const route = routes.find(route => route.path === current)
      const comp = route ? route.component : null //需要渲染的组件
      // const { routeMap, current } = this.$router
      // const comp = routeMap[current] ? routeMap[current].component : null
      return h(comp)
    }
  })
}
export default KVueRouter

这样就能实现点击那个链接,地址栏会发生变化且会渲染地址对应的组件

vuex 原理

实现 vuex 和 install 方法

先在入口文件中引入并挂载自定义 vuex

创建 kstore.js 和 index.js 并引入自定义 kvuex.js 插件、写入数据以及方法

实现 kvuex.js 插件、commit、action、getter 方法

js
let Kvue
// 实现Store 类
class Store {
  constructor(options) {
    // 保存mutations
    this._mutations = options.mutations
    // 保存actions
    this._actions = options.actions
    this._wrappedGetters = options.getters
    // 定义 computed 选项
    const computed = {}
    this.getters = {}
    // 绑定 this 到 store 实例上
    const store = this
    Object.keys(this._wrappedGetters).forEach(key => {
      // 获取用户定义的 getter
      const fn = store._wrappedGetters[key]
      // 转换为computed可以使用的无参数形式
      computed[key] = function () {
        return fn(store.state)
      }
      // 为getters定义只读属性
      Object.defineProperty(store.getters, key, {
        get: () => store._vm[key]
      })
    })
    // 响应式的 state
    this._vm = new Kvue({
      data: {
        $$state: options.state // 加了两层 $ vue 上的原有此属性会消失,保证外界不会覆盖
      },
      computed
    })

    const { commit, action } = store
    this.commit = function boundCommit(type, payload) {
      commit.call(store, type, payload)
    }
    // this.commit = this.commit.bind(store)
    this.action = function boundAction(type, payload) {
      return action.call(store, type, payload)
    }
  }
  get state() {
    return this._vm._data.$$state // this._vm._data.$$state等同于this._vm.$data.$$state
  }
  set state(v) {
    console.log('此属性不可修改')
  }
  // commit(type,payload):执行mutation,修改状态
  commit(type, payload) {
    // 根据type获取对应的mutation
    const entry = this._mutations[type]
    if (!entry) {
      console.error('不存在这个mutations' + type)
      return
    }
    entry(this.state, payload)
  }
  dispatch(type, payload) {
    const entry = this._actions[type]
    if (!entry) {
      console.error('不存在这个actions' + type)
      return
    }
    return entry(this, payload)
  }
}
// 实现插件
function install(Vue) {
  Kvue = Vue
  Vue.mixin({
    beforeCreate() {
      if (this.$options.store) {
        Vue.prototype.$store = this.$options.store
      }
    }
  })
}
// 此处导出的对象理解为 Vuex
export default { Store, install }

最后在组件中使用

这样页面中点击第一个会触发 commit 点击第二个会触发 action 还会触发第三个 getters,并且数据都是响应式的

手写 vue(vue 1.*版本)

书写使用 k-vue 所需代码,并引入 k-vue

vue
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <p>{{ counter }}</p>
      <p k-text="counter"></p>
      <p k-html="desc"></p>
    </div>
    <!-- <script src="node_modules/vue/dist/vue.js"></script> -->
    <script src="kvue.js"></script>
    <script>
      const app = new KVue({
        el: '#app',
        data: {
          counter: 1,
          desc: 'kvue<span style="color:red;">真棒</span>'
        }
      })
      setInterval(() => {
        app.counter++
      }, 2000)
    </script>
  </body>
</html>

实现 数据响应式 Observer 类、主体框架 KVue 类、模板编译 Compile 类、监视者 Watcher 类、调度者 Dep 类、数据代理函数 proxy、监听数据变化函数 defineReactive

js
// 对象响应式原理
function defineReactive(obj, key, val) {
  observe(val)
  // 每执行一次 defineReactive 就创建一个 Dep 实例
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      // console.log('get', val);
      //依赖收集
      Dep.target && dep.addDep(Dep.target)
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        // console.log('set', newVal);
        observe(newVal)
        val = newVal
        // 通知更新
        dep.notify()
      }
    }
  })
}
// 对象响应式处理
function observe(obj) {
  if (typeof obj !== 'object' || obj === null) return
  new Observer(obj)
  // Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
}
// 将 $data 中的 key 代理到KVue 实例上
function proxy(vm) {
  Object.keys(vm.$data).forEach(key => {
    Object.defineProperty(vm, key, {
      get() {
        return vm.$data[key]
      },
      set(v) {
        vm.$data[key] = v
      }
    })
  })
}

class KVue {
  constructor(options) {
    // 保存选项
    this.$options = options
    this.$data = options.data
    // 响应式处理
    observe(this.$data)
    // 代理
    proxy(this)
    // 编译
    new Compile('#app', this)
  }
}
// 每一个响应式对象,伴生一个 Observer 实例
class Observer {
  constructor(value) {
    this.value = value
    // 判断 value 是 obj 还是数组
    this.walk(value)
  }
  walk(obj) {
    Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
  }
}
// 编译过程
class Compile {
  constructor(el, vm) {
    this.$vm = vm
    this.$el = document.querySelector(el)
    // 编译模板
    if (this.$el) {
      this.compile(this.$el)
    }
  }
  compile(el) {
    // 递归遍历 el
    el.childNodes.forEach(node => {
      // 判断其类型
      if (this.isElement(node)) {
        // console.log('编译元素');
        this.compileElement(node)
      } else if (this.isInter(node)) {
        // console.log('编译差值表达式');
        this.compileText(node)
      }
      // 处理该节点的子节点
      if (node.childNodes) {
        this.compile(node)
      }
    })
  }
  // 差值文本编译
  compileText(node) {
    // 获取匹配表达式
    // node.textContent = this.$vm[RegExp.$1]
    this.update(node, RegExp.$1, 'text')
  }
  // 指令编译
  compileElement(node) {
    // 获取节点属性
    const nodeAttrs = node.attributes
    Array.from(nodeAttrs).forEach(attr => {
      // k-xxx="aaa"
      const attrName = attr.name // k-xxx
      const exp = attr.value // aaa
      // 判断这个属性类型
      if (this.isDirective(attrName)) {
        const dir = attrName.substring(2)
        // 执行指令
        this[dir] && this[dir](node, exp)
      }
    })
  }
  // 文本指令
  text(node, exp) {
    this.update(node, exp, 'text')
    // node.textContent = this.$vm[exp]
  }
  // html指令
  html(node, exp) {
    // node.innerHTML = this.$vm[exp]
    this.update(node, exp, 'html')
  }

  // 所有动态绑定都需要创建更新函数以及对应 watcher 实例
  update(node, exp, dir) {
    // 初始化
    const fn = this[dir + 'Updater']
    fn && fn(node, this.$vm[exp])
    // 更新
    new Watcher(this.$vm, exp, function (val) {
      fn && fn(node, val)
    })
  }
  htmlUpdater(node, value) {
    node.innerHTML = value
  }
  textUpdater(node, value) {
    node.textContent = value
  }
  // 判断元素
  isElement(node) {
    return node.nodeType === 1
  }
  // 判断是否是差值表达式
  isInter(node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }
  // 判断是否是指令
  isDirective(attrName) {
    return attrName.indexOf('k-') === 0
  }
}
// Watcher:小秘书,界面中的一个依赖对应一个小秘书
class Watcher {
  // vm 代表当前的 kvue 实例,key 具体是哪个地方需要改变,updateFn 代表更新函数(更新哪里)
  constructor(vm, key, updateFn) {
    this.vm = vm
    this.key = key
    this.updateFn = updateFn
    // 读一次数据,触发 defineReactive 里面的 get ()
    Dep.target = this
    this.vm[this.key]
    Dep.target = null
  }
  // 管家调用的更新函数
  update() {
    // 传入当前的最新给更新函数
    this.updateFn.call(this.vm, this.vm[this.key])
  }
}
class Dep {
  constructor() {
    this.deps = []
  }
  addDep(watcher) {
    this.deps.push(watcher)
  }
  notify() {
    this.deps.forEach(watcher => watcher.update())
  }
}

各角色功能及其联系

vue 源码分析

vue2 中一个组件对应一个 watcher 不像 vue1 中每个响应式数据对应一个 watcher 所以在 vue2 中不得不引入虚拟 dom,为了准确的区分哪里需要更新

diff

js
// 1.diff 是一种算法,由于虚拟 dom 的出现,我们需要由虚拟 dom 计算出修改 dom 的最小操作(存在新旧vdom 时才会使用 diff)
// 2.diff 算法是自顶向下同层比较,实际上就是两棵树比较
// 3.当前节点有孩子比孩子(递归 patch),如果没有子节点就比较属性和文本
// 4.准备四个指针分别指向新旧 vdom 的首位节点,四个指针会依次向中间移动(循环结束条件就是左指针位置必须在右指针左边)
// 5.首先判断新节点是否在头尾位置,如果在就直接递归 patch
// 6.如果没有则只能双循环遍历所有节点,找到当前新节点对应的旧节点再进行 patch(没找到说明是新增)
// 7.遍历结束可能会有一方节点没遍历到(如新 vdom 节点比 旧 vdom 多)这时就是批量删除或者是批量新增

模板编译

js
// 编译过程分为:解析、优化、生成
// 解析:先将模板(html模板)转换为ast
// 遇到开始标签就执行 start 函数(会用到栈,将遇到的标签字符串依次存入该结构中)
// 利用复杂的正则去匹配指令、开始结束标签、节点属性等,解析阶段会放在 webpack 中执行(vue-loader)

// 优化:标记静态节点(将来不会发生变化的节点),将来diff时可以直接跳过
// 当出现两层嵌套且html上都是固定文本无事件、属性时 转换后的 ast 上会被打上 static:true,如果是父节点还会打上staticRoot:true,方便diff跳过(不是静态节点时会打上static:false)

// 生成:将ast转换为代码字符串(渲染函数字符串,将来只需要通过 new Function(code)即可转换为函数)

典型指令 v-if、v-for 生成

js
// 带有v-if 的元素会在生成的ast中会多一个 if:foo(foo为写在模板中判断条件)、ifConditions:[]节点(ifConditions 用于记录多个判断条件)
// 最终生成的代码会是一个有多层嵌套非常复杂的三元表达式

// 带有v-for的元素会在生成的ast中会多一个for:“arr”(arr为写在模板中需要循环的数据),alias:“s”(s为别名,v-for="s in arr")

// 在带有v-if、v-for指令的元素最终生成的代码里会有一些 _c、_l、_v 等函数,这是一些函数的别名,仅用于vue内部使用,用户使用的是一些更具语义化的函数,如 render()

组件化机制

js
// vue.component() 全局注册和 vue.components() 局部注册实际上是一回事儿