Vue.js源码 阅读引导

Posted by Yinode on Friday, January 4, 2019

TOC

前言

最近一直在看 Vue.js 源码,大约花了半个多月的时间阅读,有非常多的感悟,所以在这里希望能和大家分享一下我的成果。

首先我认为 Vue.js 这种牛逼框架的源码绝对值得一读,无论是你想知道底层实现,还是想偷学编程技巧,我相信都有会很多的收获,但是其实 Vue.js 的源码阅读起来并不是没有成本的,尽管已经做了很多可读性上的优化,不过阅读早期依旧会比较懵逼,所以来讨论一下整个项目的结构,以及推荐的阅读顺序。

目录结构

src
├── compiler // 模板编译器 也就是从模板字符串到render函数的转换
├── core // Vue的核心
│   ├── components // 内建缓存组件
│   │   ├── index.js
│   │   └── keep-alive.js
│   ├── config.js
│   ├── global-api // 对Vue.js扩展一些全局API 方便进行插件化的增强
│   │   ├── assets.js  // 全局component filter 指令注册
│   │   ├── extend.js // Vue.extend
│   │   ├── index.js  // 注册全局API的入口
│   │   ├── mixin.js // Vue.mixin
│   │   └── use.js // use会调用install方法 可以进行自动化的注册 一般第三方组件都会用这个来进行注册 实际上调用的还是assets里面的方法
│   ├── index.js // 一个完整Vue构造函数的最终出口
│   ├── instance // Vue实例的各个组成部分
│   │   ├── events.js // 用来绑定实例的父子组件事件监听
│   │   ├── index.js // 入口
│   │   ├── init.js // 创建的时候初始化这里的所有东西
│   │   ├── inject.js // provide/inject 官方推荐组件库使用 使用起来可能会造成一些阅读上的额外开销
│   │   ├── lifecycle.js // 生命周期 管理一个组件的创建 更新 等等
│   │   ├── proxy.js // 在render函数中将通过proxy获取实例的属性 方便进行报错
│   │   ├── render-helpers // render函数中调用的函数 可以将模板转换出的函数调用转换成vnode
│   │   │   ├── bind-object-listeners.js
│   │   │   ├── bind-object-props.js
│   │   │   ├── check-keycodes.js
│   │   │   ├── index.js
│   │   │   ├── render-list.js
│   │   │   ├── render-slot.js
│   │   │   ├── render-static.js
│   │   │   ├── resolve-filter.js
│   │   │   └── resolve-slots.js
│   │   ├── render.js // 为组件构建_render方法 他的作用是再次调用$vm.options.render方法生成Vnode并返回
│   │   └── state.js // 管理各种数据 增加数据监听 依赖收集等
│   ├── observer // Vue数据监听的核心
│   │   ├── array.js // 对数组进行监听 原理是重写数组的原型 监听数组的所有会导致content变动的方法
│   │   ├── dep.js // 依赖收集 为每一个监听单元(一个属性)建立一个dep对象 并重写getter setter方法
│   │   ├── index.js // 入口
│   │   ├── scheduler.js // watcher run 调度器 避免疯狂刷新
│   │   ├── traverse.js // 深度watcher功能 如果需要 会自动遍历一个对象的所有内部属性 从而让dep收集完整的依赖
│   │   └── watcher.js // 依赖收集与更改响应的核心 通过dep来收集依赖到某个关联watcher 随后set时 触发这个管理watcher 后面会详细的讲 超级牛逼
│   ├── util // 一些工具 这个就略过了
│   │   ├── debug.js
│   │   ├── env.js
│   │   ├── error.js
│   │   ├── index.js
│   │   ├── lang.js
│   │   ├── next-tick.js
│   │   ├── options.js
│   │   ├── perf.js
│   │   └── props.js
│   └── vdom
│       ├── create-component.js // render函数创建组件的方法 to Vnode
│       ├── create-element.js // render函数创建普通元素 to Vnode
│       ├── create-functional-component.js // 函数式组件(其实就类似于react的函数组件 单渲染组件 自己没啥状态的) to Vnode
│       ├── helpers
│       │   ├── extract-props.js // 将父组件的PropsData属性 加载到自己的_props里
│       │   ├── get-first-component-child.js // 一个小工具 从子元素从找到第一个组件
│       │   ├── index.js // 入口
│       │   ├── is-async-placeholder.js // 工具
│       │   ├── merge-hook.js // 可以把合并Vnode的HOOK 高阶函数的玩法 有点像 FP里的 compose
│       │   ├── normalize-children.js // 规范化children 有些情况下会比较特殊 必须重新格式化称vnode数组
│       │   ├── resolve-async-component.js // 异步组件
│       │   └── update-listeners.js // 更新一个组件的监听器 会顺便绑定到_event属性上
│       ├── modules
│       │   ├── directives.js // 指令
│       │   ├── index.js
│       │   └── ref.js // ref
│       ├── patch.js // 从Vnode 到真正的DOM 就是这个Path在进行操作 递归实例化都在这里
│       └── vnode.js
├── platforms
│   ├── web
│   │   ├── entry-compiler.js
│   │   ├── entry-runtime-with-compiler.js
│   │   ├── entry-runtime.js // 一般来说 阅读从这里开始比较好(从web端的角度上说)
│   │   ├── entry-server-basic-renderer.js
│   │   ├── entry-server-renderer.js
│   │   ├── runtime
│   │   │   ├── class-util.js
│   │   │   ├── components
│   │   │   │   ├── index.js
│   │   │   │   ├── transition-group.js
│   │   │   │   └── transition.js
│   │   │   ├── directives
│   │   │   │   ├── index.js
│   │   │   │   ├── model.js
│   │   │   │   └── show.js
│   │   │   ├── index.js
│   │   │   ├── modules
│   │   │   │   ├── attrs.js
│   │   │   │   ├── class.js
│   │   │   │   ├── dom-props.js
│   │   │   │   ├── events.js
│   │   │   │   ├── index.js
│   │   │   │   ├── style.js
│   │   │   │   └── transition.js
│   │   │   ├── node-ops.js // 把DOM操作独立出来 方便移植到各个平台上

阅读前的准备

flow - Flow 是 facebook 出的 JS 静态检查工具,这东西 Vue 的作者已经隐晦的说很垃圾了,目前 3.0 正在用 TS 进行重构,所以当成普通 JS 看也没事,无非就是一些类型标注

建立一个 vue 项目,边看边输出,边测试,边打断点,爽感加倍

要有耐心,慢慢看,进度慢难理解很正常

注释非常重要 一定要仔细看

别人的理解永远是配菜,自己的理解才是主菜

善用搜索功能 JS 的点击跳转到定义的功能真的不好用 有些时候就得手动搜索

推荐的阅读顺序

也就是推荐一下,大家实际阅读的时候应该是反复跳跃性的,根本没办法真的强调顺序进行阅读。

一 Web 端的入口

/src/platforms/web/entry-runtime.js 是打包时的最终出口,推荐作为入口点. Vue.js 用了核心与底层 DOM 操作 分离的方法进行开发,这意味着能够方便的移植到多平台。

简单来说 一个 Vue 其实可以被认为是 Vue 核心+底层 DOM 操作的组合

Vue 实例

通过路线,你应该可以摸到 /src/core/instance/index.js ,在这里是一个 Vue 构造函数的组装,可以清晰的看到各个组成部分,你可以顺着这个路线看看 Vue 的实例上都拥有那些功能,他们的用途是什么。

这里的重点我认为有如下几点

  • render 方法 render 就是获取 VNode,然后交给组件的 update
  • 组件的 options 上下文
  • 数据的依赖收集过程

递归挂载

接下来你可以对 Vue 的挂载过程进行阅读 /src/core/vdom,理解递归渲染,子组件不断实例化的过程,以及当 DOM 更新到时候,vue 是如何进行 diff 的。

平台相关组件

阅读如 keep-alive ,transition 的一些组件的原理

非常牛逼的东西

这些东西我认为是 Vue.js 相比其他 MVVM 框架不同的地方,实现都非常的巧妙.

  • 依赖收集机制,按需收集,watcher 堆栈模式,数组的监听
  • diff 算法 双端对比算法 可能是最简单最容易理解的 diff 算法 大部分情况下也很高效
  • nextTick 的实现
  • patch 的过程 diff 与更新同时进行

其实我本来写了一些东西,关于 Vue 依赖收集的实现,写了一半发现实在太难写了,还不如直接看源码来的实惠,所以放弃了。大家还是靠自己看来的最好!