vue2技术揭秘笔记
本文是vue 技术揭秘的笔记 (以下统一《揭秘》指代), 除非特指,vue 都是指的 2.x 版本。 跟着大佬的脚步过了一边 vue2 底层,分析得很详细,站在巨人的肩膀上果然能够看的更高,有时间精力能力的时候必定自己读一番源码。
揭秘主要分为了数据驱动、组件化、响应式原理、编译拓展这几个部分,也分析了 vue 生态 vue router 和 vuex。
前置准备
《揭秘》最开头,介绍了 vue 的整体情况,vue2 是使用 FlowJS 做的静态类型检查,没有使用过这个库,但看介绍跟 TS 挺像的。 vue 的源码分为以下部分
src
├── compiler # 编译相关
├── core # 核心代码
├── platforms # 不同平台的支持(web和weex)
├── server # 服务端渲染
├── sfc # .vue 文件解析
├── shared # 共享代码(指浏览器端和服务端的通用代码)
Weex简单说就是一个用 web 技术开发原生应用的框架
vue 源码是基于 Rollup 构建的,从构建脚本可以看出产物有 3 种
{
"script": {
"build": "node scripts/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"build:weex": "npm run build -- weex"
}
}
关于 node 环境下的
process.argv参数,数组前两个为 node 和执行文件的路径,第三个开始依次为命令中空格相间的字符 所以web-runtime-cjs,web-server-renderer和weex这些参数是通过process.argv[2]获取的
scripts/build.js的构建过程简单来说就是:判断命令参数->过滤相应的预设构建配置->构建不同用途的 vue.js,
详细的构建过程这里不再继续展开,构建配置简单来说就是以下内容的组合:
- 模块系统,cjs 还是 esm
- 环境,如开发模式的包会包含各种日志打印,生成模式则 shake 掉了相关代码
- 运行时和编译器
- web 还是 weex
- 浏览器和服务端
- webpack 插件
vue 同时还有运行时、运行时+编译器的版本,这两者的区别简单理解就是render函数是在什么时候生成的。
vue 的最终渲染都是
render函数,该函数返回的是虚拟 dom,编译指的是template转换成render函数的过程
如果是在代码运行的时候去做编译这一过程,那么需要用的是运行时+编译器的版本,
如果在代码运行前就做好了编译工作,则只需要纯运行时版本(vue-loader就是用来体检做编译工作的)
观察运行时+编译器的产物src/platforms/web/entry-runtime-with-compiler.js,作为入口文件,做的事情十分简洁明了:
- 从
./runtime/index引入 Vue,还有编译器,以及其他的依赖 - 重写运行时 Vue 原型上的$mount 方法,主要是做了些前置工作:
- 检查挂载的根元素,不能是
html或者body - 检查渲染函数,没有渲染函数时检查
template并转换为渲染函数,没有template则取挂载元素的outerHTML作为template - 继续调用原本的$mount
- 检查挂载的根元素,不能是
- 将编译器挂载到拓展后的 Vue 的
compile属性 - 导出拓展的 Vue
接下来继续看 vue 运行时src/platforms/web/runtime/index.js,主要做了以下工作:
- 从
core/index引入 Vue - 拓展 Vue 原型,设置
__patch__和$mount属性,还有杂七杂八的
再进到src/core/index.js:
- 从
./instance/index引入Vue对象,引入initGlobalAPI和其他工具 - 调用
initGlobalAPI初始化全局 Vue API - 拓展
Vue.prototype,定义了$isServer和$ssrContext - 拓展
Vue,定义了FunctionalRenderContext - 标记版本并导出
Vue
进到src/core/instance/index.js,可以看到Vue是一个函数类,需要用new来实例化
(并且内部做了判断,Vue只能作为构造方法使用),在这个类导出之前,有一系列的xxxMixin方法对这个类的原型进行一系列的拓展,
这也是Vue没有使用 ES6 的 Class 实现的原因:方便维护和管理。
function Vue(options) {
if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
warn("Vue is a constructor and should be called with the `new` keyword");
}
this._init(options);
}
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);
说回initGlobalAPI,它作用是Vue.prototype进行了一系列的方法拓展后,对Vue对象本身拓展全局静态方法,
先挂载了以下属性到Vue上:
- util
- set
- delete
- nextTick
- options
然后是下列的初始化操作:
initUse(Vue);
initMixin(Vue);
initExtend(Vue);
initAssetRegisters(Vue);
这里需要注意下流程中对原型的拓展和对原对象的静态拓展,在原型上拓展的内容,是 vue 实例才能访问到的, 而在
Vue上拓展的静态全局 API,则需要通过Vue对下岗来访问
大致过完了框架的大流程,接下来就是核心的分析了
数据驱动
数据驱动作为 vue 的核心已经老八股文了,面试经常会问,而我也是经常那几句话:
- 数据劫持,
defineProperty - 依赖收集,发布订阅
- 啊吧啊吧…
实例化
哈哈,光知道这些肯定不够的,知其然知其所以然,《揭秘》首先带我们继续深扒 vue 的实例化过程,
回到Vue构造函数,可以看到在判断完是否作为构造方法调用后,紧接着调用了this._init,
而这个初始化方法正是initMixin在Vue.prototype上拓展的,主要做了以下工作:
- 合并配置项
- 初始化生命周期(给 vm 实例加上各种生命周期的标记)
- 初始化事件中心(根据 parent 更新自己的 listener)
- 初始化渲染
- 初始化 data、props、computed、watcher 等
代码中可以清晰的看到beaforeCreate和created的两钩子前后发生了什么:
//...
initLifecycle(vm);
initEvents(vm);
initRender(vm); // 声明了vm.$createElement等
callHook(vm, "beforeCreate");
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, "created");
//...
挂载
相关的细节并没有继续深入,接下来是vm的挂载过程,上文也提到运行时版本的入口platform/web/runtime/index.js,
对Vue.prototype上的$mount进行了拓展,$mount内部则是在查询到元素后,最终调用了lifecycle的mountComponent方法,
并同时传入vm实例,然后:
- 检查
vm的选项式 api 是否有render,没有的话调用createEmptyVNode - 触发
beforeMount钩子 - 实例化
Watcher,其回调函数调用vm._render生成虚拟 node,然后用vm._update更新 DOM - 标记
vm._isMounted,触发mounted钩子
render
通过对挂载过程的分析可以看出较为关键的是vm._render和vm._update,
这两个都是实例私有方法,是在准备工作中提到的renderMixin和lifecycleMixin对Vue.prototype拓展的,
Vue.prototype._render的关键操作:
- 挂载父节点到
vm.$vnode - 调用实例上的
render方法(即options上的,可能是经过template转换而来)
createElement
源码中可以注意到在调用实例的 render 方法是这种姿势
vnode = render.call(vm._renderProxy, vm.$createElement);
而这个vm.$createElement是由vm._init->initRender声明在vm上的,最终调用的是
src/core/vdom/create-element.js
略去一些细节(有点复杂了,不再这里展开),它做的主要工作就是:
- 规范化 children
- 创建 VNode
update
从挂载的源码可知vm._update调用的时机为首次渲染和数据更新时,它是在lifecycleMixin中拓展的,
它做的事情就是经典的差异比对vm.__patch__(平台相关的一个方法,所以是在platform/web/runtime/中定义的),
platform/web/runtime/platch.js又表明这个方法是src/core/vdom/patch.js返回的,
patch 很复杂 😭😭,记一些关键点:
- 支持的 nodeOps 是因平台而异的
- 关于子节点的处理是深度优先的,created 钩子深自底向上触发,插入顺序也是如此
组件化
在调用createElement的过程中,会判断是否组件,调用createComponent:
- 构造子类构造函数(通过原型继承的方法,组件导出的对象继承 Vue 的一个子类,并缓存)
- 安装组件钩子函数
- 实例化
vnode
TODO 组件patch的差异
组件的注册方式分为全局注册和局部注册,实际上globalApi中初始化了['component','directive','filter']
三个全局函数
异步组件支持:
- 通过普通的工厂函数
- promise
- 高级异步组件,通过 promise 定义需要加载的组件,并可定义加载中和加载错误的组件,和等待时间 delay、超时时间 timeout 等,
本质上是两次渲染(delay 为 0 第一次直接渲染 loading 组件,否则第一次渲染一个注释节点,异步组件后获取成功后,
通过
forceRender强制重新渲染
响应式原理

响应式对象创建过程
记录一下关键点:
- 实例化->
vm._init->initState->初始化props、methods、data、computed、watcher等 - 初始化
props:遍历props- 调用
defineReactive把每个prop对应的值转换为响应式,使得vm._props.xxx能访问到对应属性 - 调用
proxy把访问vm._props.xxx的访问代理到vm.xxx上
- 调用
- 初始化
data:遍历data函数返回的对象- 调用
proxy把每一个值vm._data.xxx都代理到vm.xxx上 - 调用
observe方法观测整个data的变化,将data变成响应式
- 调用
-
proxy方法:通过defineProperty把target[[sourceKey][key]]的读写变成target[key]的读写, 使得vm.xxx能够访问到vm._props.xxx和vm._data.xxx -
observe方法:- 传入值不是对象或是一个 VNode 对象则 return 掉
- 声明一个
Observer - 检查对象是否添加了
Obsever,如果添加了,取这个Observer - 没有添加
Observer的话,在满足一定条件下,实例化一个Observer - 返回这个
Observer
-
Observer类,用来给对象的属性添加 getter 和 setter,即传说中的依赖收集和派发更新:- 构造函数先实例化一个
Dep对象 - 执行
def函数把自身实例添加到入参对象value的__ob__属性上 - 判断
value是否数组,是则调用observeArray方法- 遍历数组再次调用
observe方法
- 遍历数组再次调用
- 否则调用
walk方法- 遍历
value对象的 key 调用defineReactive方法
- 遍历
- 构造函数先实例化一个
-
defineReactive的功能是定义一个响应式对象,入参是对象和属性名等,给对象动态添加 getter 和 setter:- 初始化一个
Dep对象实例, - 获取入参
obj的属性描述符,对子对象地柜调用observe方法 - 定义 getter 和 setter
- 初始化一个
依赖收集
上面的提到的Dep对象其实就是依赖收集的核心,它在源码中是个 class,类上有个全局静态属性target,
是一个全局唯一的Watcher,Dep实际上是对Watcher的一种管理
Watcher也是一个 Class,这个类有很多属性,和Dep相关的有:
this.deps = []; // 当前Watcher实例持有的Dep实例数组
this.newDeps = []; // 新添加的依赖数组
this.depIds = new Set();
this.newDepIds = new Set();
回想 Vue 的 mount 过程,在调用mountComponent函数时,实例化了一个Watcher,大致发生了这些事:
- 进入
Watcher构造函数逻辑this.get()pushTarget(this)
- 将
Watcher实例赋值Dep.target并放入栈中 - 触发
Wacher的第二个入参函数updateComponent- 执行
vm._update,触发vm._render() - 生成 VNode 的过程触发
vm上的数据访问 - 触发数据对象的
getter
- 执行
- 调用数据对象的
dep.depend()即Dep.target.addDep(当前dep实例)即watcher.addDep()- 按照条件更新
watcher的newDeps和newDepsIds,并执行dep.addSub - 调用
traverse递归访问,触发子项getter - 调用
popTarget和清空依赖
- 按照条件更新
//TODO 此处应该有张图 😭😭😭😭😭
依赖收集这个过程是一个非常经典的观察者模式,这里回顾一下观察者模式和发布订阅模式的区别: 观察者模式通常是观察者和发布者直接通向,而发布订阅模式在两者之间多了一个主题/时间通道, 发布者向通道发布主题或者事件,订阅者向通道进行订阅,由通道触发事件与订阅者通信, 其目的是避免发布者和订阅者产生依赖关系。
派发更新
记录下大致过程:
- 修改响应数据,触发 setter 逻辑
- 更新值
- 调用
dep.notify() - 遍历
subs数组里的watcher实例 - 调用
watcher的update方法 - 区分
computed、sync以及其他执行不同逻辑,此时会走到最后的queueWatcher
- 把这些
watcher添加到一个队列,nextTick后执行flushSchedulerQueue- 自顶向下排列
watcher - 遍历
watcher执行watcher.run()-
this.get()获取当前值,触发组件渲染patch过程 - 判断是否满足新旧值不相等、新值是对象类型、deep 模式开启任何一个条件
- 执行
watcher回调
-
- 恢复状态,清空队列
- 自顶向下排列
nextTick
回顾事件循环:
- 所有同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,还存在一个”任务队列”。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
- 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。之前那些 往”任务队列”放事件的异步任务,会结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步骤。
src/core/util/next-tick.js单独维护了nextTick,关于timmerFunc的取值遵循以下顺序
- Promise
- MutationObserver
- setImmediate
- setTimeout
特殊情况
这也是一些 vue2 的”特色”
-
Object.defineProperty实现的响应式对象,新增属性无法触发 setter,vue 为此专门提供了set方法 - 无法监测数组的索引操作、length 操作,并且重写了数组的一些方法使其实现响应式
[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ], 并且获取了能增加数组长度的方法的插入值,将其转换为响应式对象,最后在方法内部主动调用ob.dep.notify()
计算和侦听
计算属性本质上是 computed watcher,而侦听属性本质上是 user watcher。
就应用场景而言,计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;
而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。
组件更新
// TODO Diff 算法
编译
简单说就是 AST 语法树转换->标记静态节点->可执行代码

拓展
这一章节都只拖到后面看了下总结…
Vue Router
vue 插件注册原理
vue 提供了Vue.use全局 API 来注册插件,它是在src/core/global-api/use.js中维护的:
-
Vue.use接受一个plugin参数,并在内部维护一个_installedPlugins数组 - 判断
plugin有没有定义install方法,有的话会调用这个方法,并在所有入参前加一个Vue参数 - 把插件储存到数组中
vue-router 的 install
当用户执行Vue.use(VueRouter)的时候,实际上是在执行VueRouter的install函数:
-
install函数在自身维护了一个install.installed标记来确保插件只被安装一次, 并缓存了Vue入参 - 使用
Vue.mixin把 beforeCreate和destroyed`钩子函数注入到每一个组件 - 一些初始化工作,定义
vm.$route和vm.$router - 通过
Vue.component方法定义全局<router-lint>和<router-view>组件
VueRouter 对象
VueRouter是个 ES6 class,以下是一些关键属性
this.app = null; // Vue实例
this.apps = []; // 持有$options.router属性的Vue实例
this.options = options; // 传入的路由配置
this.beforeHooks = []; // 一些钩子
this.resolveHooks = [];
this.afterHooks = [];
this.matcher = createMatcher(options.routes || [], this); // 路由匹配器
let mode = options.mode || "hash";
// 判断是否回退到hash模式
this.fallback =
mode === "history" && !supportsPushState && options.fallback !== false;
if (this.fallback) {
mode = "hash";
}
if (!inBrowser) {
mode = "abstract";
}
this.mode = mode; // 路由模式
// history的不同实现分支
switch (mode) {
case "history":
this.history = new HTML5History(this, options.base);
break;
case "hash":
this.history = new HashHistory(this, options.base, this.fallback);
break;
case "abstract":
this.history = new AbstractHistory(this, options.base);
break;
default:
if (process.env.NODE_ENV !== "production") {
assert(false, `invalid mode: ${mode}`);
}
}
// TODO matcher history.transitionTo
Vuex

最后
非常浅的过了一遍大佬的《vue 技术揭秘》,说实话没太吃透,暂时只在脑海中建立了 vue 的总体框架, 不太建议一上来就读这个,还是得先对 vue 有一定的理解。