Appearance
利用 $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() 局部注册实际上是一回事儿