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.将编写的内容转换成如下结构
编写
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(`
//配置zhufeng-cli create命令
program
//create命令,
.command('create
//描述
.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
//执行zhufeng-cli create --help可以看见具体命令的详情介绍
program.on('--help',function () {
console.log();
console.log(`Run ${chalk.cyan(`zhufeng-cli
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模板