1.使用Rollup搭建环境
(1).初始换项目
npm init -y
(2).安装依赖
npm i rollup rollup-plugin-babel @babel/core @babel/preset-env --save-dev
- rollup 打包工具,打包后比webpack5体积更小
- rollup-plugin-babel 在rollup中使用babel插件
- babel/core babel 核心
- babel/preset-env 编译语法
(3).新建rollup.config.js
rollup配置文件,使用rollup启动
"scripts": {
"dev": "rollup -cw"
},
(4)修改rollup.config.js
//rollup默认可以导出一个对象,作为打包的配置文件
import babel from 'rollup-plugin-babel'
export default{
input:'./src/index.js', //入口
output:{
file:'./dist/vue.js', //出口
name:'Vue',//global.Vue
format:'umd', //esm es6模块 commonjs模块 iife自执行函数 umd(统一模块规范)
sourcemap:true, //希望可以调试源代码
},
plugins:[
babel({
exclude:'node_modules/**',//排除node_modules所有文件
})
]
}
新建babel配置.babelrc
{
"presets": ["@babel/preset-env"] //使用@babel/preset-env
}
配置完成后npm run dev 启动打包
(5)使用打包好后的js
在dist目录下新建index.html,导入vue.js
2.初始化数据
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="vue.js"></script>
<script>
//响应式的数据变化,数据变化了我可以监控到数据的变化
const vm=new Vue({
data(){ //代理数据
return{
name:'张三',
age:20
}
}
})
</script>
</body>
</html>
3.vue2核心流程
- 创建了响应式数据
- 模板转换成ast语法树
- 将ast语法树转换了render函数
- 后续每次数据更新只执行render函数 无需再次执行ast转换的过程
render函数会产生虚拟节点(使用响应式数据)
根据生成的虚拟节点创建真实的DOM
4.vue2核心api实现原理
(1).computed(计算属性原理)
- 计算属性 依赖的值发生变化才会重新执行用户的方法 计算属性中要维护一个dirty属性,默认计算属性不会立刻执行
- 计算属性就是一个defineProperty
- 计算属性也是一个watcher,默认渲染会创造一个渲染watcher
- 底层就是一个带有dirty属性的watcher
(2).watch(watch实现原理)
- 底层就是你写的是watch的方式,也会被转化成$watch的写法
(3).array(数组响应式原理)
- 给数组本身增加Dep 如果数组新增了某一项 我可以触发dep更新
- 给对象也增加dep,如果后续用户增添了属性 我可以触发dep更新
- 重写数组的方法,内部调用原来的方法,函数的劫持,切片编程
5.diff算法
在之前的更新中每次更新,都会产生新的虚拟节点,通过新的虚拟节点生成真实节点,生成后替换来的节点
现在第一次渲染的时候我们会产生虚拟节点,第二次更新我们也会调用render方法产生新的虚拟节点,对比出需要更新部分内容
let render1 = compileToFunction(`<div style="color:red">{{name}}</div>`)
let vm1 = new Vue({ data: { name: '张三' } })
let pervNode = render1.call(vm1)
let el = createElm(pervNode)
document.body.appendChild(el)
//如果用户自己操作dom,可能会有些问题
let render2 = compileToFunction(`<div style="background:blue">{{name}}</div>`)
let vm2 = new Vue({ data: { name: '李四' } })
let nextVNode = render2.call(vm2)
//直接将新的节点替换掉了老的,不是直接替换,而是比较区别之后在替换
//希望比较差异去更新 希望比较差异去更新 diff算法是一个平级比较的过程 父亲和父亲比对 儿子和儿子比对
setTimeout(() => {
patch(pervNode,nextVNode)
}, 1000)
diff算法核心逻辑
function updateChildren(el, oldChildren, newChildren) {
//vue2中采用双指针的方式 比较两个节点
let oldStartIndex = 0
let newStartIndex = 0
let oldEndIndex = oldChildren.length - 1
let newEndIndex = newChildren.length - 1
let oldStartVnode = oldChildren[0]
let newStartVnode = newChildren[0]
let oldEndVnode = oldChildren[oldEndIndex]
let newEndVnode = newChildren[newEndIndex]
function makeIndexByKey(children) {
let map = {}
children.forEach((children, index) => {
map[children.key] = index
})
return map
}
let map = makeIndexByKey(oldChildren)
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { //有任何一个不满足停止
if (!oldStartVnode) {
oldStartVnode = oldChildren[++oldStartIndex]
} else if (!oldEndVnode) {
oldEndVnode = oldChildren[--oldEndIndex]
} else if (isSameVnode(oldStartVnode, newStartVnode)) {
//双发有一方头指针,大于尾部指针则停止循环
patchVnode(oldStartVnode, newStartVnode) //如果是相同节点 则递归比较子节点
oldStartVnode = oldChildren[++oldStartIndex]
newStartVnode = newChildren[++newStartIndex]
//比较开头节点
}
else if (isSameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode) //如果是相同节点 则递归比较子节点
oldEndVnode = oldChildren[--oldEndIndex]
newEndVnode = newChildren[--newEndIndex]
//比较开头节点
}
//交叉比对 abcd->dabc
else if (isSameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode) //如果是相同节点 则递归比较子节点
//insertBefore是具有移动性,会将原来的元素移动走
el.insertBefore(oldEndVnode.el, oldStartVnode.el) //将老的尾部移动到老的前面
oldEndVnode = oldChildren[--oldEndIndex]
newStartVnode = newChildren[++newStartIndex]
}
else if (isSameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode) //如果是相同节点 则递归比较子节点
//insertBefore是具有移动性,会将原来的元素移动走
//nextSibling是一个属性,它获取节点的下一个同级节点。如果选定node的nextSibling属性不存在,那么意味着这个node是其父元素的最后一个子节点。
el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling) //将老的尾部移动到老的前面
oldStartVnode = oldChildren[++oldStartIndex]
newEndVnode = newChildren[--newEndIndex]
} else {
//乱序比较
//根据老的列表做一个映射关系,用新的去找,找到则移动,找不到则添加,最后多余的删除
let moveIndex = map[newStartVnode.key] //如果拿到则说明是我要移动的索引
if (moveIndex !== undefined) {
let moveVnode = oldChildren[moveIndex] //找到对应的虚拟节点
el.insertBefore(moveVnode.el, oldStartVnode.el)
oldChildren[moveIndex] = undefined //表示这个数组已经移动走了
patchVnode(moveVnode, newStartVnode)//比对子节点
} else {
el.insertBefore(createElm(newStartVnode), oldStartVnode.el)
}
newStartVnode = newChildren[++newStartIndex]
}
}
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) { //插入多的
let childEl = createElm(newChildren[i])
let anchor = newChildren[newEndIndex + 1] ? newChildren[newEndIndex + 1].el : null //获取下一个元素
//向前或向后追加
el.insertBefore(childEl, anchor)//anchor为null的时候会认为appendChild
// el.appendChild(childEl)
}
}
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) { //移除多的
if (oldChildren[i]) {
let childEl = oldChildren[i].el
el.removeChild(childEl)
}
}
}
}
6.vue2 API
Vue.component 作用就是收集全局的定义 id和对应的definition Vue.options.components[组件名]=包装成构造函数(定义)
vue.extend 返回一个子类,而且会在子类上记录自己的选项 (为什么Vue的组件中的data不能是一个对象呢?)
如果data是对象就是引用类型,如果是函数调用函数返回最新值
function extend(选项){
function Sub(){
this._init() //子组件的初始化
}
Sub.options=选项
return Sub
}
let Sub=Vue.extend({data:数据源})
new Sub() mergeOptions(Sub.options) Sub.options.data() //如果data是一个对象 就是共享的
new Sub() mergeOptions(Sub.options) Sub.options.data()
- 创建子类的构造函数的时候,会将全局的组件和自己身上定义的组件进行合并 (组件的合并 会先查找自己再查找全局)
- 组件的渲染 开始渲染组件会编译组件的模板变成render函数 -> 调用render方法
- createElement 会根据tag类型来区分是否是组件,如果是组件会根据组件的虚拟节点(组件增加初始化钩子,增加componentOptions选项{ctor})稍后创建组件的真实节点 我们只需要new Ctor()
- 创建真实节点
7.vue2源码(github)目录结构
- bechmarks 性能测试
- dist 打包结果
- examples 官方的例子
- flow 类型检测(没人用了 和 ts功能类似)
- packages 一些写好的包
- scripts 所有打包的脚本都放这里
- src 源代码目录
- compiler 专门用作模板编译的
- core vue2的核心代码
- plateforms
- shared 就是模块之间的共享属性和方法
- 通过package.json找到打包入口,找到入口文件
- scr/plateforms/web/entry-runtime-with-compiler.ts
- runtime/index.ts (所谓运行时 会提供一些Dom操作的api 属性操作、元素操作,提供一些组件和指令)
- core/index initGlobalAPI初始化全局API
- core/instance/index Vue的构造函数
- 扩展原型方法
initMixin(Vue) //Vue.prototype._init
stateMixin(Vue) // Vue.prototype.$set Vue.prototype.$delete Vue.prototype.$watch
eventsMixin(Vue) // Vue.prototype.$on Vue.prototype.$once Vue.prototype.$off Vue.prototype.$emit
lifecycleMixin(Vue) //Vue.prototype._update Vue.prototype.$forceUpdate Vue.prototype.$destroy
renderMixin(Vue) // Vue.prototype.$nextTick Vue.prototype._render
8.重点记录
- Object.defineProperty 实现数据劫持
- 模板引擎的实现原理就是 with + new Function
- 为什么要组件化(复用、方便维护、局部更新)
- nextTick 没有直接使用某个api 而是采用优雅降级的方式
- diff算法是一个平级比较的过程
- Vue.component 作用就是收集全局的定义