1.vue2源码篇
Vue是一个借鉴MVVM框架开发思想,但并不完全是MVVM框架,因为MVVM的核心思想是数据变化视图会更新,视图变化数据会被影响
不能跳过数据去更新视图,然而通过$ref可以直接操作DMO更新视图。
1)使用Rollup搭建开发环境
1.什么是Rollup?
一个js打包工具,只针对js。一般开发一个完整的项目会选择webpack,开发一个js库会选择rollup。
2.环境搭建
1)安装rollup环境
npm install rollup rollup-plugin-babel @babel/core @babel/preset-env rollup-plugin-serve -D
rollup-plugin-babel:在rollup和babel之间搭建桥梁的插件。
@babel/core:编译ES6以上语法的核心库。
@babel/preset-env:里面有一些预设,装了一些将ES6转成ES5的插件。
rollup-plugin-serve:开启本地服务插件。
2)rollup.config.js文件编写
3)配置.babelrc文件
{
"presets":[
"@babel/preset-env"//将ES6转成ES5插件
]
}
4)配置package.json文件,执行脚本配置
"scripts": {
//-c代表使用rollup脚步运行rollup.config.js文件,-w代表只要代码一改变就运行脚步
"dev": "rollup -c -w"
},
2)Vue响应式原理
1.vue的初始化流程(初始化数据)
1)在index.js文件中扩展构造函数原型方法,将Vue类传给一个一个扩展插件,在插件中扩展Vue原型方法并将配置对象options挂在实列对象上,
在Vue类中调用原型方法并将配置对象options传递过去
2)在init.js文件中扩展初始化实例方法,在state.js文件中编写初始化实例状态 data props watch computed等逻辑
3)初始化实例状态data
1.取出配置对象的data属性判断是否是函数,如果是函数调用使用call()改变this指向为实例对象,如果是对象就直接返回
2.将data传给劫持函数observe(),并且把data挂在实列_data属性上(使用Object.defineProperty()方法完成data数据的劫持方案 )
4)总结
1.导出vue构造函数
2.init方法中初始化vue状态
3.根据不同属性进行初始化操作
4.(上面三条描述)将配置对象options挂在实列对象上,将data props watch computed等属性挂在实列对象上,调用observe()方法对data属性进行劫持监视。
2.vue对象类型的拦截(递归属性劫持)
1)在observe/index.js文件中完成data数据的劫持方案
2)observe()函数中对data类型进行判断,如果不是对象或者是null不进行拦截,封装Observer类进行数据劫持
3)Observer类中调用walk()和defineReactive()和Object.defineProperty()重新定义data上的属性
(如果data中第二层还是对象或者用户将data属性改为对象都要调用observe()方法进行递归劫持)
4)总结
observe()方法只对对象和数组进行劫持,对象通过Object.defineProperty()方法进行递归深度监视(设置对象属性时也进行深度监视)
3.vue中数组的方法的拦截(数组方法的劫持,在observer/array.js文件中完成)
1)我们开发功能时很少对数组索引进行操作,同时为了性能考虑不对数组进行拦截 ,拦截可以改变数组的方法进行操作
2)在Observer类中做个判断如果data是数组,那么在observer/array.js文件中重写能改变原数组的数组方法
在observeArray()方法中对数组对象元素进行劫持
3)通过push/unshift/splice方法给数组增加对象属性需要用observeArray()方法进行数据监视,然后observeArray()
在Observer类中,所以在劫持前给value扩展一个不能被枚举的__ob__属性,值为Observer类实例,这样就可以调用observeArray()方法
4)总结
1.重写了能改变原数组的数组方法
2.增加__ob__属性
通过push/unshift/splice方法给数组增加对象属性需要用observeArray()方法进行数据监视,数据被监视是给数据增加__ob__属性,
作用是避免重复监视和让数组可以调用observeArray()方法进行监视数组中的对象元素
4.数据代理(在state.js中完成)
封装代理方法proxy(),当去实例上面取值或者设置值都去data上面取值设置值
5.总结
1)暴露Vue类,扩展Vue原型上的方法抽取出来在其它文件上,new Vue类时调用init方法初始化实例data等属性
2)初始化data等属性时,将属性代理到实例上,方便用户操作,并且调用observer()方法对属性进行拦截监视
3)拦截对象时,调用Object.defineProperty()方法重新定义data等属性上的属性进行监视,当data第二层属性还是对象
或者将data属性修改为对象时继续调用observer()方法对属性进行拦截监视
4)拦截数组时,调用observeArray()方法,拦截监视数组的对象元素,并且重写了数组上能改变原数组的方法,在调用push
unshift/splice等方法给数组增加元素时,取到新增元素调用observeArray()方法对新增的对象元素进行监视
5)监视属性时给属性增加__ob__属性,让被监视的属性不在被重复监视,重写数组文件中可以调用observeArray()方法
3)模板编译
1.渲染流程
1)默认会先找render()方法的返回值作为模板进行解析,如果找不到render()方法就会找template,在找不到就会找
el属性(等价于vm.$mount('#app'),定义el属性最终会调用$mount()方法)指定的模板,只要找到一个模板就不在往下找了
2)template模板最终会被ast语法树(语法树就是用对象描述js语法)转成render()方法,el属性最终会转换成template模板
2.模板编译(在compiler和init.js文件下完成)
1)如果当前有el属性就调用vm.$mount()方法解析模板
2)如果没有render()函数并且没有template模板有el属性指定的元素,那么取到el元素赋值给template模板,调用compileToFunctions()
将方法template模板转换成render()函数赋值给实例
3)解析标签和内容,生成ast语法树
1.调用parseStartTag()方法拿到开头标签的标签名和标签属性数组传给解析开始标签start()方法,并把html中拿到的部分删除掉
2.拿到结束标签名传给end()方法并删除标签结构
3.拿到标签文本传给chars()方法并删除标签文本
4.开始返回ast语法树,在createASTElement()方法中处理标签树,在end()方法中辨识标签的父亲与孩子,在chars()方法中将文本推到标签孩子中
4)将ast语法树生成代码,生成render()函数
1.将编写的内容转换成如下结构
编写<div id="app" style="color:red"> hello {{name}} <span>hello</span></div>
结果:render(){
return _c('div',{id:'app',style:{color:'red'}},_v('hello'+_s(name)),_c('span',null,_v('hello')))
}
2.在genProps()方法中转义属性,返回的是对象,对style属性做特殊处理,值也是对象
3.在genChildren()方法中做生成儿子处理,儿子如果是标签直接调用generate()进行转义,儿子如果是文本,那么就使用
匹配插值语法正则将正常字符串和插值语法放到数组里面,再用+号拼接成字符串
4.将代码生成render()函数
3.将render函数返回值生成虚拟dom(在init.js和lifecycle.js和vdom文件中)
1)在init.js文件中调用mountComponent()方法完成组件挂载,在mountComponent()方法中先调用_render方法创建虚拟节点
2)在vdom/index.js文件中完成生成完整虚拟dom(创建元素虚拟dom/创建文本虚拟dom/解析文本变量)
3)在lifecycle.js文件中的_update()方法将虚拟dom变成真实dom替换掉老的dom节点
4)总结
1)获取el属性模板,在实例没有上没有template模板和render()函数情况下,将el属性模板给template模板
2)将template模板转换成描述html语法的ast树,标记静态节点(就是把一些不可变的节点进行标记,这样可以减少新旧节点的比较,不过这个没有实现),
在把ast树生成字符串js代码
3)将字符串js代码转换成render函数生成虚拟dom
4)将虚拟dom变成真实dom替换掉老的dom节点
4.总结vue的渲染流程
先初始化数据 =》 将模板进行编译 =》 render函数 =》 生成虚拟节点 =》 生成真实的dom替换老的节点 =》 扔到页面上
4)生命周期的合并(mixin)
生命周期就是回调函数, 先订阅好后续到一定的阶段在触发此方法
1.在global-api/index.js和在util.js文件中的mergeOptions()方法合并全局options配置
2.在init.js文件中调用mergeOptions()方法合并全局和实例options配置
3.在lifecycle.js文件中编写调用生命周期函数callHook()方法
4.在init.js文件初始化数据(initState)前后调用beforeCreate()和created()生命周期
5.在lifecycle.js文件中挂载组件(mountComponent)前后调用beforeMount()和mounted()生命周期函数
6.总结
合并全局options配置,在初始化数据之前合并全局和实例options配置,在初始化流程中调用生命周期
5)依赖收集
vue更新策略是以组件为单位的,给每个组件都增加了一个watcher唯一标识,属性变化后会重新调用这个watcher来重新渲染组件
1.在lifecycle.js文件中组件挂载前(mountComponent)给组件增加了一个watcher唯一标识
2.在observer/watcher.js文件中调用get()方法,实际上调用vm._update(vm._render()),这里什么都没有做只是把
vm._update(vm._render())封装到watcher的get()方法中
3.在observer/watcher.js文件中调用get()方法渲染前将warcher挂在Dep上
4.在oberver/index.js文件中重新定义属性前(defineProperty)创建一个Dep实例,在get()方法渲染模板取值时,调用
dep实例的depend()方法将dep存到watcher的deps数组中同时给每个dep加给唯一标识id避免存重复dep,存完dep调用addSub将watcher放在dep的subs数组中
在set方法设置值时,调用dep实例的notify()方法通知watcher调用get()方法重新渲染页面
5.数组更新问题
1)在observer/index.js文件中Observer类构造方法给对象或数组增加一个dep对象,当递归监视对象属性时拿到observe()方法
的返回值(Observer实例),在get()方法取值时,如果是数组那么就调用dep.depend()方法订阅更新,再在observer/array.js
文件中调用数组方法是调用dep.notify()通知数组更新
6.总结
1)组件挂载前给组件增加了一个watcher唯一标识,在watcher的getter()方法放更新页面的方法
2)模板在取值时调用dep.depend()方法将watcher和dep进行依赖关联,在设置值时调用dep.notify()方法通知watcher更新页面
3)给数组属性在设置值时调用dep.depend()方法将watcher和dep进行依赖关联,在调用完数组方法后调用dep.notify()方法通知watcher更新页面
6)实现Vue异步批量更新之nextTick
1.在observer/watcher文件中当页面更新是调用update()和queueWatcher()方法watcher放在queue数组中并去重处理
2.在observer/watcher和util.js和state.js文件中调用nextTick()和$nextTick()方法批量异步更新页面
3.nextTick()方法是把用户更新数据引起页面更新的回调(这里面是一个watcher就调用一次更新方法)和用户调用$nextTick()方法的回调放到一个数组
里面,再去异步批量防抖处理
4.总结
1)数据变化时将不同的watcher放在一个数组里面,使用防抖调用watcher.run()方法异步更新页面
2)将异步更新页面的逻辑封装在nextTick()方法中,当内部调用nextTick()方法和用户调用$nextTick()方法,将nextTick()方法
的回调放在一个数组里面去批量异步处理。
7)watch的实现原理
1.用法
watch: {
// 1.value为函数
a(){}
// 2.value为数组
a:[function a1(){},function a2(){}]
// 3.value为字符串,aa为methods上的方法
a:'aa'
// 4.value为对象
'a.a.a':{
handler(newValue,oldValue){
console.log(newValue,oldValue, '内部watch')
}
},
}
2.在state.js文件中的initWatch()方法初始化watch属性,最终调用vm.$watch()监视属性
3.在state.js文件中Vue.prototype.$watch()方法中创建warcher实例,如果immediate为true直接调用一次回调函数
4.在observer/watcher.js文件中如果创建的是用户watcher实例,那么this.getter函数的逻辑是在实例上取监视属性的值
并把拿到的老值存在watcher实例上
5.在watch.run()渲染方法中拿到监视属性的新旧值,如果是用户watcher就调用cb回调函数
6.总结
用户设置watch,相当于是创建了一个watcher实例并且把监视的属性老值放在watcher实例上,取值时同时也做了依赖收集
当渲染完成后会拿到监视属性的新旧值,如果是用户watch就会调用回调并存入监视属性的新旧值
8)diff算法
1.基本diff算法
1)在vdom/patch.js文件中的patch函数做新旧节点替换操作和diff算法,默认初始化时 是直接用虚拟节点创建出真实节点来 替换掉老节点
在更新的时 拿老的虚拟节点 和 新的虚拟节点做对比 ,将不同的地方更新真实的dom
2)对比标签
比较两个元素的标签 ,标签不一样直接拿新的节点替换掉老的dom节点即可
3)比对文本
文本的tag为undefind,如果节点的tag为undefind,那么比较内容,如果内容不一致就替换掉即可
4)对比属性
调用updateProperties()方法对比新旧节点属性,老的有新的没有 需要删除属性,样式属性做特殊循环比较处理,新的有 那就直接用新的去做更新即可
5)对比子元素
1.新的没有,把老的孩子删掉;老的没有,直接把新的孩子插到节点中;老的有儿子 新的也有儿子调用updateChildren()方法更新孩子
2.新老节点头对头比
拿到新老节点的第一个和最后一个孩子,依次从第一个开始向后比较,调用isSameVnode()方法如果两个节点的标签名和key值一样说明两个是
同个标签,同个标签调用patch()方法来更新文本、属性、再去递归遍历孩子;
3.新老节点尾对尾比
拿到新老节点的第一个和最后一个孩子,依次从最后一个开始向前比较,调用isSameVnode()方法如果两个节点的标签名和key值一样说明两个是
同个标签,同个标签调用patch()方法来更新文本、属性、再去递归遍历孩子;
4.比较完成后如果新节点孩子长度大于老节点孩子长度,那么判断对比到的索引的后面一个元素有没有值,如果有值说明是从后往前比较的,否则说明是从前往后比较
接着调用insertBefore()和createElm()方法向前或者向后插入多余的元素
5.老节点头部对新节点尾部比
老节点头部对新节点尾部比,如果相同,将老的头部元素插入到尾部的 下一个元素的前面,老的开始指针向后移,新的结束指针向前移
6.老节点尾部对新节点头部比
老节点尾部对新节点头部比,将老的尾部元素插入到头部元素的前面,老的结束指针向前移,新的开始指针向前后
7.暴力对比
1)拿新的节点每一个跟老的节点去对比(在makeIndexByKey()方法将老节点key和index做个映射,便于比对),没有找到的话,那么将新的节点插到老的节点第一个的
前面找到的话将老的节点移到老的节点第一个的前面,并且移到的节点位置致为空,循环比对完后就,将老的开始结束指针中间的元素删除掉即可
2)因为在暴力比对时会把需要移动的老的节点元素位置致为null,所以在while循环时如果老的开始结束指针为null时就直接跳过下一次
比对,优化比对次数
6)在lifecycle.js文件中的_update()方法区分一下 到底是首次渲染还是更新,如果是首次渲染给patch()方法传真实模板和虚拟节点
那虚拟节点生成的真实节点代替老的节点即可,如果是更新,那么给patch()方法传老新虚拟节点,复用老的节点
2.总结
1.在patch函数做新旧节点替换操作前做diff算法来复用老模板可以复用的节点
2.比较两个元素的标签 ,标签不一样直接拿新的节点替换掉老的dom节点即可
3.比对文本,如果内容不一致就替换掉即可
4.调用updateProperties()方法对比新旧节点属性,老的有新的没有 需要删除属性,新的有 那就直接用新的去做更新即可
5.对比子元素
1)依次从第一个开始向后比较新老节点,如果是同个标签,调用patch()方法来复用老节点文本、属性、再去递归遍历孩子;
2)依次从最后一个开始向前比较新老节点,如果是同个标签,调用patch()方法来复用老节点文本、属性、再去递归遍历孩子;
3)比较完成后如果新节点孩子长度大于老节点孩子长度,那么判断对比到的索引的后面一个元素有没有值,如果有值说明是从后往前比较的,否则说明是从前往后比较
接着调用insertBefore()和createElm()方法向前或者向后插入多余的元素
4)老节点头部对新节点尾部比
老节点头部对新节点尾部比,如果相同,将老的头部元素插入到尾部的 下一个元素的前面,老的开始指针向后移,新的结束指针向前移
5)老节点尾部对新节点头部比
老节点尾部对新节点头部比,将老的尾部元素插入到头部元素的前面,老的结束指针向前移,新的开始指针向前后
6)暴力对比
拿新的节点每一个跟老的节点去对比,没有找到的话,那么将新的节点插到老的节点第一个的前面找到的话将老的节点移到老的节点第一个的前面,并且移到的节点位置致为空,循环比对完后就,
将老的开始结束指针中间的元素删除掉即可
9)computed实现原理
1.用法
computed:{
//取值调用这个方法
fullName(){
return this.firstName + this.lastName
}
}
computed:{
fullName:{
//取值调用这个方法
get(){},
//设置值调用这个方法
set(){}
}
}
2.在state.js文件中的initComputed()方法初始化Computed属性,循环Computed上的属性,在defineComputed()方法中
使用Object.defineProperty()把Computed上的属性重新定义在实例上,Computed属性如果是个函数那么这个函数就是取值时
的get()方法,如果是对象,那么对象的get()和set()方法就是取值和设置值时的get()和set()方法
3.实现缓存
1)在重新定义computed属性时给每个属性增加一个watcher实例,这个watcher实例上有个lazy和dirty属性,代表这是一个计算属性watcher和
是否处于缓存状态,并把用户计算属性的函数或对象的get()方法传给watcher实例,作为watcher实例的getter()方法
2)将去实例上取计算属性时执行的get()方法封装到createComputedGetter()和watcher.evaluate()方法中
3)在observer/watcher.js文件的update()方法,当更改计算属性依赖的属性将dirty致为true,页面重新渲染就可以获得最新的值了
4.计算属性依赖的属性收集渲染watcher
1)在observer/dep.js文件中的pushTarget()方法收集所有类型的watcher,在popTarget()方法中当计算属性watcher执行完后
将watcher拿出去,剩下的就是渲染watcher
2)在state.js文件中的createComputedGetter()方法做判断,如果Dep.target中还watcher,那么就调用watcher.depend()方法
让计算属性依赖的属性去收集渲染watcher
5.总结
1)在state.js文件中的initComputed()方法初始化Computed属性,把Computed上的属性重新定义在实例上,Computed属性如果是个函数那么这个函数就是取值时
的get()方法,如果是对象,那么对象的get()和set()方法就是取值和设置值时的get()和set()方法
2)在重新定义computed属性时给每个属性增加一个watcher实例,这个watcher实例上有个lazy和dirty属性,,并把用户计算属性的函数或对象的get()方法传给watcher实例,
作为watcher实例的getter()方法
3)将去实例上取计算属性时执行的get()方法封装到createComputedGetter()和watcher.evaluate()方法中,此时计算属性依赖的属性会收集这个计算属性watcher
4)在observer/watcher.js文件的update()方法,当更改计算属性依赖的属性将dirty致为true,页面重新渲染就可以获得最新的值了
5)在watcher.depend()方法让计算属性依赖的属性收集渲染watcher
10)Vue.extend用法
Vue.extend内部会继承Vue的构造函数,Vue.extend返回值是Vue的子类,我们可以自己进行实例化操作,并且手动挂载到指定的位置
let childComponent=Vue.extend({
template:'<div>hello world {{msg}}',
data(){
return {msg:'zf'}
}
})
new childComponent().$mount('result')
11)组件的解析
1.为什么要拆分成小的组件?
1) 实现复用
2) 方便维护
3) vue的更新是以组件为单位的,拆分成小组件可以减少diff比对
2.全局组件
1)在global-api文件中的Vue.component方法中给组件对象扩展一个name属性,默认是用户写的name属性,没有就拿组件id作为name
调用Vue的extend()方法(就是this.options._base.extend)创建一个Vue子类,接着将组件id和子类以键值对的方式放在Vue.options.components属性上
2)在global-api文件中的Vue.extend方法产生一个组件子类来继承父类Vue
3)在util文件中的strats.components方法,定义一个res属性,res属性的__proto__指向父类,再把子类的components属性定义在res属性上
这样寻找组件加载时就会先去子类上找再去父类上找
4)在vdom/index.js文件中的createElement()方法创建虚拟节点时做个判断如果标签是html标签那么就走正常创建虚拟节点的流程即可
如果标签是组件那么调用createComponent()方法去创建组件的虚拟节点,生成组件的虚拟节点多了componentOptions属性里面包含Ctor,children(插槽)
data里面多了hook对象,里面包含组件的初始化方法init()
5)在vdom/patch.js文件中createElm()和createComponent()方法,如果标签是组件,那么要返回组件的真实dom,组件的真实dom是通过组件init()方法挂在vnode.componentInstance.$el上
3.总结
1)调用Vue的extend()方法创建一个Vue子类,接着将组件id和子类以键值对的方式放在Vue.options.components属性上
2)在util文件中的strats.components方法,定义一个res属性,res属性的__proto__指向父类,再把子类的components属性定义在res属性上
这样寻找组件加载时就会先去子类上找再去父类上找
3)在vdom/patch.js文件中createElm()和createComponent()方法,如果标签是组件,那么要返回组件的真实dom,组件的真实dom是通过组件init()方法挂在vnode.componentInstance.$el上
12)源码分析
1.文件目录分析
1)dist
1.vue.common.js node版本
2.vue.esm.js es6版本
3.vue.common.dev.js 开发版
4.vue.common.prod.js生产版
2)examples 用vue写代码的案例
3)flow 声明文件
4)scripts 打包配置的一些脚本
5)src 源码目录
6)test 单元测试
7)src/core/instance/index.js源码路口
2.$set()方法的实现
1)在src/core/observer/index.js文件中的set()方法中,如果给数组增加元素,那么使用splice()方法添加,这样新增的元素就具备响应式
如果给对象增加属性,如果增加的属性是原来有的属性,那么直接赋值即可,如果增加的对象时vue实例那么报错,如果增加的目标对象不是响应式的也不能增加响应式属性
否则调用defineReactive()方法增加响应式属性,并调用ob.dep.notify()方法手动通知视图的更新
3.$delete()方法的实现
1)和$set()方法一样,数组用splice()方法删除,对象用delete删除
4.Vue.observable()方法的实现
在在src/core/global-api/index.js文件中的observable()方法将传入的对象用observe()方法监视,在返回对象
5.Vue.use()方法的实现
// 将Vue构造函数传给插件的install()方法,在方法中将插件的属性挂载vue实例上即可
// Vue.use = function (plugin) {
// plugin.install(this);
// }
2.Vue服务端渲染
概念:放在浏览器进行就是浏览器渲染,放在服务器进行就是服务器渲染。
1)浏览器渲染和服务器渲染的区别
1.服务端渲染是可以被爬虫抓取到的,客户端异步渲染是很难被爬虫抓取到的,客户端渲染不利于 SEO 搜索引擎优化
2.SSR直接将HTML字符串传递给浏览器。大大加快了首屏加载时间。SSR会占用CPU和内存资源,所有不建议把所有的工作交给ssr。
3.一些常用的浏览器API可能无法正常使用,比如说vue的生命周期,在vue中只支持beforeCreate和created两个生命周期
!! 4.服务端渲染只解决首屏加载速度,只有在浏览器url地址输入地址才会触发服务端渲染,路由的跳转时不会触发服务端渲染
2)服务端渲染流程
webpack打包项目,以服务端和客户端两个入口开始打包,打包生成两个bundle,服务端将生成的字符串html返回给客户端,客户端再解析字符串,将事件挂在html上
3)搭建项目
//vue-server-renderer将vue的代码解析成浏览器可以解析的代码
//@koa/router服务端路由
//nodemon改变node代码自动刷新
//concurrently运行多个命令
npm i vue vue-server-renderer koa @koa/router nodemon concurrently
4)总结
1.webpack打包项目,以服务端(src/server-entry.js)和客户端(src/client-entry.js)两个入口开始打包,打包出来两份代码供服务端和客户端使用
2.在server.js文件中以dist/vue-ssr-server-bundle.json和dist/index.ssr.html文件服务端将生成的字符串html返回给客户端,
并将客户端的js代码dist/a.bundle.js插入到html中,在a.bundle.js中会调用app.$mount('#app')挂载组件
5)服务端渲染路由
在server.js文件中在路由渲染前端模板时将路由路径传给src/server-entry.js文件下的context,然后默认跳转路由,等路由加载完毕在返回app即可
6)服务端请求数据和vuex的使用
在server-entry.js文件中通过router.getMatchedComponents()方法获取到匹配到的路由,判断路由中有没有asyncData()方法,有的话就调用将store传给
asyncData()方法,同时通过context.state = store.state操作将服务器更新过后的放在window.__INITIAL_STATE__上,然后在create-store.js文件中更新前端store.state
3.项目实战 (一)
1)文件目录
src
api 方法请求
assets 放静态资源
components 普通组件
plugins 插件使用
router 路由配置
store vuex
utils 工具
views 路由组件
2)路由配置
1.在router/routers目录下放路由模块的配置
2.在router/index.js文件下使用require.context()动态引入路由
3)页面整体布局实现
1.在App.vue文件中实现上中下布局
2.使用el-menu实现头部布局和路由跳转
4)vuex配置
1.store/modules文件下放vuex子模块,rootModule.js文件下放根模块
2.在index.js文件中使用require.context()动态引入模块
5)axios封装
1.utils/axios.js文件下封装Http类,constructor()中放默认配置,mergeOptions90()方法合并配置,setInterceptor()方法设置公共拦截器
request()方法创建axios实例并传入全部配置,每次请求都会创建唯一实例,这样可以针对每个实例去配置自己的拦截器,get()/post()方法get和set方法
2.在api文件下封装各个模块请求,config.js文件下配置接口地址
6)home组件
1.实现轮播图,请求数据进行展示,在mounted()方法中做轮播图缓存,如果vuex中有轮播图数据就不发送请求
7)login组件
1.搭建login组件静态页面
2.在utils/local.js文件中封装本地存取方法
3.login组件组件中判断用户有没有在本地存uuid,没有创建uuid,然后发送请求获取svg验证码进行展示
4.拿到登录参数发送请求将用户信息存到vuex中,同时将用户token存到本地,在utils/axios.js文件下将token放到请求头中
5.在router/hooks.js文件中增加loginPermission钩子对刷新页面检测登录权限,如果有token携带token发送请求去后端拿最新数据保存到vuex中
给vuex中增加hasPermission属性标识是否登录状态,在loginPermission钩子里面做导航鉴权
8)请求显示加载组件和切换组件取消请求功能
1.utils/axios.js文件下setInterceptor()方法把请求放在queue队列中,当queue队列个数有值显示加载组件,响应成功时删除queue队列对应请求
当queue队列个数没值取消加载组件
2.utils/axios.js文件下setInterceptor()方法给每个请求匹配一个取消请求的方法放在vuex中,在router/index.js的beforeEach()方法中当进入
路由前取消所有请求并清空vuex取消请求列表
9)菜单权限
在router/hooks.js文件中增加menuPermission钩子,去动态添加角色可以访问的菜单路由
10)动态菜单
在components/ManagerMenu.jsx文件中动态生成菜单
11)按钮权限
在main.js文件中定义全局权限指令,将指令放到按钮上,没有权限的按钮不会显示
12)要素总结
1.使用require.context()动态引入路由
2.使用require.context()动态引入vuex模块
3.axios封装
4.切换组件取消请求功能
5.登录鉴权
6.菜单权限/动态菜单
7.按钮权限
4.vuex源码篇
1)vuex基本用法
vuex中的state属性在各个组件中共享,在组件中可以dispatch action异步去后台获取数据,在action中可以commit mutations去同步更新state
2)install()方法的实现
在vuex/store.js文件中的install()和applyMixin()方法中将根组件的store属性赋值给子孙组件的$store属性
3)modules的实现
1.收集模块
1)在vuex/state.js文件中通过ModuleCollection类中的register()方法进行模块收集,建立上下级模块关系
2)在vuex/module/module.js文件中将模块封装成一个类,进行扩展属性和方法,方便模块的收集
2.模块的安装
1)在vuex/state.js文件中通过installModule()方法安装模块,在vuex/module/module.js文件中通过给Module类
扩展forEachMutation()/forEachAction()/forEachGetter()/forEachChild()来安装模块,将所有模块上的
actions、mutation、getters 都把他定义在根模块上面
3)vuex state和getters的实现
1.在vuex/state.js文件中的installModule()方法将所有的子模块的状态安装到父模块的状态上
2.在vuex/state.js文件中的resetStoreVM()方法将state属性经过vue实例响应式代理后在将vm实例挂载Store实例上,当去取Store实例上的
state属性时就去vm实例上取((在属性访问器get上)),这样state属性就是响应式的
3.在vuex/state.js文件中resetStoreVM()方法将getters属性的值全部放在vue实例的computed属性上,然后当去Store实例上getters属性值时就会去取computed属性上
的值调用getters属性相应方法,这样就取到值并且做到缓存效果
4)actions和mutations的实现
在vuex/state.js文件中的dispatch()和commit()方法去调用相应的actions和mutations数组回调即可
5)命名空间的实现
在vuex/state.js文件中的installModule()方法在安装模块actions、mutation、getters时通过getNamespaced()方法获取到标有命名空间模块的前缀再去安装模块
6)vuex的插件和subscribe()和replaceState()方法的的实现
1.插件里面放着一个一个函数,在vuex加载时会调用里面的函数
2.subscribe()方法在mutation调用后执行回调函数,参数是个回调,回调参数mutation和state
3.replaceState()方法会替换掉state状态,参数就是要替换的state
4.在vuex/state.js文件中扩展subscribe()方法将用户订阅的subscribe回调放在store实例_subscribes数组属性上,在installModule()方法中当调用mutaion是去
调用_subscribes数组属性上的回调
5.在vuex/state.js文件中扩展replaceState()方法替换掉vm实例上的$$store属性,同时在installModule()方法中通过getState()方法去获取最新的state
7)辅助函数
1.在vuex/helper.js文件中写辅助函数方法,辅助函数的逻辑就是依次去store里面取数组里面的state/getter/action/mutation然后返回一个对象,
2.在vuex/index.js文件中用export导入,不能用export default导入,因为export default导入的不能解构赋值
8)总结
1.加载vuex,在install()方法通过Vue.mixin()方法中的beforeCreate()生命周期将vue根实例store全部注入给子孙组件
2.模块收集,在ModuleCollection类中进行模块收集,建立上下级模块关系
3.模块加载,在installModule()方法将所有模块上的actions、mutation、getters 都把他定义在根模块上面,将所有的子模块的状态安装到父模块的状态上
4.state/getter,在resetStoreVM()方法将state属性经过vue实例响应式代理后在将vm实例挂载Store实例上,当去取Store实例上的state属性时就去vm实例上取,这样state属性就是响应式的
将getters属性的值全部放在vue实例的computed属性上,然后当去Store实例上getters属性值时就会去取computed属性上的值调用getters属性相应方法,这样就取到值并且做到缓存效果
5.命名空间,在installModule()方法在安装模块actions、mutation、getters时通过getNamespaced()方法获取到标有命名空间模块的前缀再去安装模块
6.subscribe()和replaceState()方法,在installModule()方法中当调用mutaion是去调用_subscribes数组属性上的回调,在replaceState()方法替换掉vm实例上的$$store属性,
同时在installModule()方法中通过getState()方法去获取最新的state
7.辅助函数,辅助函数的逻辑就是依次去store里面取数组里面的state/getter/action/mutation然后返回一个对象
5.路由篇
1)路由分类
1.hash模式
前端需要根据路径hash值不同显示对应的内容,由于路径中多了#号比较不好看,所以上线时一般采用history模式
// 主要依赖hashchange事件监听hash值变化,然后执行回调函数加载对应页面
// let fn = function () {
// app.innerHTML = window.location.hash
// };
// fn();
// window.addEventListener('hashchange',fn)
2.history模式
history模式是根据浏览器提供的history对象来实现的,根据对应的路径来显示对应的内容,由于实现时通过前端history对象来实现的,
所有刷新时访问的路径后端并不存在,所以会报错,真正上线时需要后端配合。
// 主要依赖pushState和popstate事件实现,不同的是需要服务器
// let fn = function () {
// app.innerHTML = window.location.pathname
// };
// function goA(){
// 参数1地址对应的加载内容,参数2标题,参数3地址
// history.pushState({},null,'/a');
// fn();
// }
// function goB(){
// history.pushState({},null,'/b');
// fn();
// }
// 浏览器前进后退时触发popstate事件
// window.addEventListener('popstate',function () {
// fn();
// });
2)路由的使用
1.当Vue.use(Router)时,内部会提供给router-link/router-view这两个全局组件和$router/$route这两个属性
2.当在那个vue实例上注入router,那么那个实例就会有router-link/router-view这两个全局组件和$router/$route这两个属性
3)install()方法的实现
在vue-router/install.js文件中使用Vue.mixin()方法将router实例放在vm根实例上,然在再把vue根实例放到所有的组件上面
这个所有的组件就可以拿到router实例了
4)创建路由映射表
1.在vue-router/index.js文件中的createMatcher()和createRouteMap()方法创建路由映射表,在createMatcher()方法中有
addRoutes()和match()方法用来动态添加路由和根据路径匹配对应组件
5)路由跳转(这里只实现hash模式)
1.在vue-router/index.js文件中根据路由配置选项的mode属性确认路由模式去创建对应的路由模式类
2.在vue-router/index.js文件中的init()方法transitionTo()方法获得当前hash值(getCurrentLocation方法获取当前hash值)进行跳转,
并且监听hash变化(setUpHashListener方法监听hash变化加载对应组件)匹配对应组件
6)根据路由路径匹配组件,一个路径可能匹配多次组件
1.在vue-router/history/base.js文件中的History类的构造函数中通过createRoute()方法返回路径对应的匹配到的路由数组
2.在vue-router/history/base.js文件中的transitionTo()方法中的this.router.match()和create-matcher.js文件下的
match()和createRoute()方法返回路径变化时对应的匹配到的路由数组
3.在vue-router/history/base.js文件中的transitionTo()方法中当新匹配到的路径与老的路径一样并且匹配到的路由长度一样,那么不更新视图
不一样的话就改变当前匹配到的路由current,在vue-router/install.js文件中使用defineReactive()方法将current挂在vue根实例的_route
上属性,这样就变成响应式的,在vue-router/index.js文件中的init()方法中的history.listen()方法当路由变化时更新_route属性
7)实例上$route和$router的实现
在vuex/install.js方法中使用Object.defineProperty()给Vue.prototype扩展两个属性$route和$router,取值时取vue根实例上的_route和_router
8)router-view和router-link的实现
1.在vue-router/components/link.js和view.js文件中定义router-view和router-link组件,在install.js文件中使用Vue.component()方法注册这两个组件
2.在link组件中给组件标签绑定点击事件,事件中调用router实例的push()和history类的push()方法和history类父类的transitionTo()方法进行跳转和改变url的hash值
3.在vue-router/components/view.js文件中拿到router-view组件所对应的组件渲染即可
9)总结
1.install()方法的实现,在vue-router/install.js文件中使用Vue.mixin()方法将router实例放在vm根实例上,然在再把vue根实例放到所有的组件上面
这个所有的组件就可以拿到router实例了
2.动态添加路由和路由匹配,在vue-router/index.js文件中通过createMatcher()方法给router实例添加matcher属性,属性上有两个方法addRoutes()/match()用来
动态添加路由和根据路由获取所有匹配到的路由
3.根据路由模式设置url/监视url/匹配路由,在vue-router/index.js文件中的init()方法中根据用户设置的路由模式去创建对应的路由模式类,获得当前hash值进行跳转, 并且监听hash变化匹配对应组件
4.匹配到的路由响应式化,在vue-router/install.js文件中使用defineReactive()方法将current挂在vue根实例的_route上属性,这样就变成响应式的,
在vue-router/index.js文件中的init()方法中的history.listen()方法当路由变化时更新_route属性
5.$route和$router的实现,在vuex/install.js方法中使用Object.defineProperty()给Vue.prototype扩展两个属性$route和$router,取值时取vue根实例上的_route和_router
6.router-view和router-link的实现,在install.js文件中使用Vue.component()方法注册router-view和router-link这两个组件,在link组件中给组件标签绑定点击事件,进行跳转和改变url的hash值
在vue-router/components/view.js文件中拿到router-view组件所对应的组件渲染即可
6.vue3源码篇
1)vue3使用
1.main.js文件使用
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
2.vuex使用
import { createStore } from 'vuex'
const store = createStore({
state: {},
getters: {},
mutations: {},
actions: {}
})
export default store;
3.路由使用
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
//createWebHistory代表路由模式是history路由
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
4.setup()方法使用
//每个组件挂载调用一次,props父级传递过来的属性,context相当vue实例
setup(props, context) {}
5.响应式数据
import { reactive, toRefs} from "vue";
setup(props, context) {
const state = reactive({
selectedKeys: 2
});
return {
...toRefs(state), // 保证数据是响应式的 还有解构的功能,页面中可以直接使用selectedKeys属性
};
import { ref } from "vue";
//单独将某个属性变成响应式的
ref(store.getters.allTime)
6.watch
import {watch} from "vue";
//参数1监视的属性,参数二执行的回调,参数3配置对象
watch(()=>route.path,(newValue)=>{
state.selectedKeys = [newValue]
},{immediate:true})
7.computed
import {computed} from "vue";
const = selectedKeys: computed(() => {
return [route.path]}
7.vue面试题
1)请说一下响应式数据的理解?
1.vue实例在初始化data等属性时,会对data属性上的对象和数组属性进行劫持监视,对象内部通过Object.defineProperty将属性进行劫持(只会劫持已经存在的属性)
多层对象是通过递归来实现劫持
2.拦截数组时,内部通过Object.defineProperty方法拦截监视数组的对象元素,并且重写了数组上能改变原数组的方法,在调用push
unshift/splice等方法给数组增加元素时,取到新增元素调用通过Object.defineProperty方法对新增的对象元素进行监视
3.在数据劫持中也会做依赖收集,每个属性都拥有自己的dep属性,存放他所依赖的watcher,当属性变化后会通知自己对应的watcher去更新
4.也会给整个对象和整个数组增加自己的dep属性,在调用push、unshift/splice等方法给数组增加元素时,通知自己对应的watcher去更新
5.补充
1)对象层级过深,拦截监视属性性能就会差,不需要响应数据的内容不要放到data中
2)修改数组的索引和长度是无法监控到的。需要通过以上7种变异方法修改数组才会触发数组对应的watcher进行更新。如果想更改索引更新数据
可以通过Vue.$set()来进行处理,核心内部用的是splice方法
2)Vue中模板编译原理?
1)初始化数据后会获取el属性模板,在实例没有上没有template模板和render()函数情况下,将el属性模板给template模板
2)将template模板转换成描述html语法的ast树,标记静态节点(就是把一些不可变的节点进行标记,这样可以减少新旧节点的比较,不过这个没有实现),
在把ast树生成字符串js代码
3)将字符串js代码转换成render函数生成虚拟dom
4)将虚拟dom变成真实dom替换掉老的dom节点
8.单元测试
1)测试框架
Karma:可以让你的项目跑在不同的浏览器里,这样可以直观的看到页面,一般用来测试样式
Mocha:不提供浏览器,只提供一个自动化测试的框架,内置了断言库chai asset(1==1,'出错了'),没办法测试样式
Jest:fecebook推出的,集成了mocha + jsdom(node环境模拟dom环境),问题是无法测试样式 ,自带测试覆盖率 独立的测试框架 0配置 直接用即可
vue/test-utils:vue提供的一个测试vue组件库样式的测试包,提供了丰富的api,和karma一起使用测试样式,也可以和Jest一起使用,但是不能测试样式
2)Karma+Mocha使用
1.安装Karma
npm i --save-dev @vue/test-utils karma karma-chrome-launcher karma-mocha karma-sourcemap-loader karma-spec-reporter
karma-sourcemap-loader karma-webpack mocha karma-chai
2.配置karma.conf.js文件
3.在package.json文件中scripts字段增加"test": "karma start "命令
9.vue组件库实现
1)在src/packages/index.js文件中install()方法将组件注册成全局组件,然后在main.js文件中引入使用即可
2)button和icon和button-group组件
1.通过计算属性给button加上类型样式btnClass
2.通过插槽给button加上文字内容
3.通过symbol方式使用icon图标,根据用户传入的类型展示对应图标
4.通过order属性控制图标在button中的前后顺序
5.实现button-group组件,并且通过console.assert()方法实现断言,孩子必须是button元素
3)搭建Karma+Mocha测试环境(一般不会用)
1.安装Karma
npm i --save-dev @vue/test-utils karma karma-chrome-launcher karma-mocha karma-sourcemap-loader karma-spec-reporter
karma-sourcemap-loader karma-webpack mocha karma-chai
2.配置karma.conf.js文件
3.在package.json文件中scripts字段增加"test": "karma start "命令
4.tests/unit/button.spec.js文件下测试组件,文件必须以.spec.js结尾
4)打包成类库
1.在package.json文件中scripts字段增加打包命令,在main字段中增加引用库的入口文件
//--target lib打包成类库,--name zhu-ui库名字,./src/packages/index.js打包输出位置
"lib": "vue-cli-service build --target lib --name zhu-ui ./src/packages/index.js"
5)生成项目文档
1.在zhu-ui-doc文件中生成package.json文件
2.下载vuepress
npm i vuepress -D
3.配置package.json运行命令
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs"
4.下载其他插件
//会使用到element-ui,所以下载
"element-ui": "^2.13.0",
//代码高亮
"highlight.js": "^9.18.1",
//项目使用sass来写样式,所以安装的两个根sass相关的插件
"node-sass": "^4.13.1",
"sass-loader": "^8.0.2"
5.增加入口文件的README.md
//文件夹和文件名规定好的了
在docs文件夹下创建README.md文件
6.配置.vuepress文件夹(这边代码下载下来没有)
6)row和col组件
1.在row组件中通过计算属性给组件添加样式,根据justify属性分配项目富裕空间
2.在row组件mounted()方法中,将gutter属性传给孩子,孩子中用data接收,因为组件只能更改data属性,props属性不能修改会报错
3.在col组件中通过计算属性给组件添加类名,offset偏移量,span占用份额,"xs", "sm", "md", "lg", "xl"响应式布局
7)container组件
1.container组件孩子中有"zh-footer"或 "zh-header"垂直布局,否则水平布局,并且设置flex:1
2.header/footer组件默认高度60px,main组件设置flex:1,padding:20px,aside组件默认宽度300px
8)input组件
1.组件中接收父级传来的vaue绑定给input框,input框值改变时@input="$emit('input',$event.target.value)"来实现双向数据绑定
2.通过定位来增加前后icon图标
3.在data中增加passwordVisible属性来控制密码的显示和隐藏,点击图标时失去焦点,在获取焦点,保证光标保留在value后面
4.清空值时,防止事件传播来让input框依然获取焦点
9)upload上传组件
1.通过监听格式化用户传入的文件fileList
2.组件中用户点击上传按钮,清空输入框,触发输入框的点击事件开始选择文件
3.选中文件会触发input框change事件,在uploadFiles()方法中处理上传文件逻辑
4.判断文件大小是否超过限制大小,超过触发用户的onExceed()方法,handleStart()方法中格式化文件内容,触发用户onChange()事件
在upload()方法中触发用户的beforeUpload()事件对文件的大小和类型进行校验,校验成功通过post()方法上传文件
5.搜集发送请求参数发送请求,发送请求过程中改变文件状态触发用户onChange()事件
6.progress.vue文件中实现进度条,upload-dragger.vue文件中实现拖拽上传文件
10)时间组件
1.点击输入框显示时间框,点击输入框和时间框以外的区域隐藏时间框
2.在data中定义time属性表示输入框显示的时间,tempTime属性表示时间框可以修改的时间
3.在计算属性中通过获取当月的第一天和这天是周几,直接将当前时间向前移动多少天后 开始循环42天,来展示日期面板
4.给非当月,当日,选中日加上日期,点击时间框改变输入框值,改变输入框日期进行日期校验改变时间框值
5.实现年月框
11)vue虚拟列表(有现成的vue-virtual-scroll-list插件实现了)
1.引用组件时传入总共需要渲染的列表条数,可视区内渲染的条数,每条的高度
2.组件中拿到可视区内渲染的条数进行渲染即可,使用作用域插槽将每条列表传给用户
3.根据滚动条滚动过的距离计算,开始和结束的列表项,同时增加前后预留项进行渲染,渲染是让列表选位移到正确位置上
4.传的列表每项不知道高度的情况
1)添加variable表示传的列表每项不知道高度,但是依然要给预估高度size
2)通过initPosition()方法缓存预估的每项位置信息
3)在滚动滚动条时,通过getStartIndex()方法二分查找找到开始元素的位置,然后通过计算属性updated重新计算每项缓存位置信息,最后计算出最新滚动条高度
4)滚动条滚动事件节流这边没有写,可以用lodash库实现
10.脚手架
1)先创建可执行的脚本D:\学习资料\前端资料\前端架构师资料\01.jiagouke2-vue\9.zhufeng-cli\bin\zhu
//用node环境运行脚本
#! /usr/bin/env node
2)配置package.json 中的bin字段
//脚本运行的文件
"bin": "./bin/zhu",
3)npm link 把本地包放在全局,这样就可以在全局运行脚本了 (默认以name为基准)
//默认package.json的name字段运行脚本,可以配置成对象更改脚本名
//zhufeng-cli就会执行zhu文件
zhufeng-cli
4)配置脚手架可用命令
npm install commander
//D:\学习资料\前端资料\前端架构师资料\01.jiagouke2-vue\9.zhufeng-cli\bin\zhu文件
program
.version(`zhufeng-cli@${require('../package.json').version}`) //脚手架版本,取package.json文件version字段
.usage(`<command> [option]`) //可执行命令标题,zhufeng-cli --help可以看见所有命令
//配置zhufeng-cli create命令
program
//create命令,<app-name>为命令参数可以在action回调第一个参数拿到
.command('create <app-name>')
//描述
.description('create a new project')
//命令参数
.option('-f, --force', 'overwrite target directory if it exists')
//在action中实现命令功能
.action((name, cmd) => {
// 调用create模块去创建
require('../lib/create')(name, cleanArgs(cmd))
// 我需要提取这个cmd中的属性
})
//zhufeng-cli --help可以看见Run `zhufeng-cli <command> --help` show details字段话
//执行zhufeng-cli create --help可以看见具体命令的详情介绍
program.on('--help',function () {
console.log();
console.log(`Run ${chalk.cyan(`zhufeng-cli <command> --help`)} show details`)
console.log();
})
5)实现create命令:D:\学习资料\前端资料\前端架构师资料\01.jiagouke2-vue\9.zhufeng-cli\lib\create.js
inquirer:控制台使用命令时,弹出的可选项库
1.create.js文件中下载模板时先判断模板是否存在再去调Creator类创建模板
2.fetchRepo方法获取所有git模板名
3.fetchTag方法获取所有git模板版本
4.download下载git模板