1.使用Rollup搭建环境

手写vue2源码地址

(1).初始换项目

 npm init -y

(2).安装依赖

npm i rollup rollup-plugin-babel @babel/core @babel/preset-env --save-dev
  1. rollup 打包工具,打包后比webpack5体积更小
  2. rollup-plugin-babel 在rollup中使用babel插件
  3. babel/core babel 核心
  4. 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核心流程

  1. 创建了响应式数据
  2. 模板转换成ast语法树
  3. 将ast语法树转换了render函数
  4. 后续每次数据更新只执行render函数 无需再次执行ast转换的过程
    render函数会产生虚拟节点(使用响应式数据)
    根据生成的虚拟节点创建真实的DOM

4.vue2核心api实现原理

(1).computed(计算属性原理)

  1. 计算属性 依赖的值发生变化才会重新执行用户的方法 计算属性中要维护一个dirty属性,默认计算属性不会立刻执行
  2. 计算属性就是一个defineProperty
  3. 计算属性也是一个watcher,默认渲染会创造一个渲染watcher
  4. 底层就是一个带有dirty属性的watcher

(2).watch(watch实现原理)

  1. 底层就是你写的是watch的方式,也会被转化成$watch的写法

(3).array(数组响应式原理)

  1. 给数组本身增加Dep 如果数组新增了某一项 我可以触发dep更新
  2. 给对象也增加dep,如果后续用户增添了属性 我可以触发dep更新
  3. 重写数组的方法,内部调用原来的方法,函数的劫持,切片编程

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)目录结构

vue2源码地址

  • 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

image.png

8.重点记录

  1. Object.defineProperty 实现数据劫持
  2. 模板引擎的实现原理就是 with + new Function
  3. 为什么要组件化(复用、方便维护、局部更新)
  4. nextTick 没有直接使用某个api 而是采用优雅降级的方式
  5. diff算法是一个平级比较的过程
  6. Vue.component 作用就是收集全局的定义