主要目录结构
scripts:构建相关的脚本和配置文件
dist:构建后的文件
flow:Flow的类型声明
packages:vue-server-render 和 vue-template-compiler,它们作为单独的NPM包发布
test:所有的测试代码
examples:实例代码
types:TypeScript 类型定义
src:源代码
- compiler : 与模版编译相关的代码
- core : Vue核心库(通用的、与平台无关的运行时代码)
- observer :实现变化侦测的代码
- vdom :实现虚拟dom的代码
- instance: Vue.js实现实例的构造函数和原型方法
- global-api :全局静态API
- components :通用的抽象组件
- server SSR : 服务端渲染相关代码
- platforms : 特定平台相关代码(实现跨平台)
- sfc : 单文件组件(*.vue文件)解析逻辑
- shared : 整个项目的公用工具代码
Flow
一般在大型项目中我们需要使用静态类型检查来确保代码的可维护性和可读性,所以vue2中引入的flow,flow可以让代码在最小改动的情况下使用静态类型检查
- Flow的静态类型检查错误是通过静态类型推断实现的
- 文件开头通过 // @flow或者/* @flow */声明
/* @flow */ function square(n: number): number { return n * n; } square("2"); // Error!
- 文件开头通过 // @flow或者/* @flow */声明
调试设置
打包
- 打包工具Rollup
- Vue.js源码的打包工具使用的是Rollup,比webpack轻量
- Webpack把所有文件当做模块,Rollup只处理js文件更适合在Vue.js这样的库中使用(react中用的打包工具也是rollup)
- Rollup打包不会生成冗余的代码
在dist下有打包后生成的文件,基本上就是不同版本的vue
- 前两行是未压缩版本,后两行是压缩版本(生产版本)
- Full是完整版本,Runtime-only是运行时版本
- 完整版:同时包含编译器和运行时的版本
- 编译器:用来将模版字符串编译成render渲染函数的代码,体积大,效率低。(在创建vue时,会传入一个template的选项,template选项中指明了模版,编译器的作用是把template转换成javascript的渲染函数,也就是render函数,render函数的作用是生成虚拟dom)
- 运行时:用来创建vue实例、渲染并处理虚拟dom等的代码,体积小,效率高。基本上就是去除编译器的代码
- UMD:UMD版本通用的模块版本,支持多种模块方式(支持CommonJS,amd,还有直接将js挂载在全局对象上,可以在浏览器环境下直接运行)vue.js默认文件就是运行时+编译器的UMD版本
- CommonJS CommonJS用来配合老的打包工具,比如Browserify或webpack 1(在node中经常使用)
- ESModule:从2.6开始Vue会提供两个ES Modules构建文件,为现代打包工具提供的版本
- ESM格式被设计为可以被静态分析(也就是在编译去处理打包过程中的依赖,而不是运行的时候),所以打包工具可以利用这一点来‘tree-shaking’并将用不到的代码排除出最终的包(tree-shaking将项目中使用不到的代码剔除掉)
基于vue-cli创建的项目,默认导入的vue就是运行时版本并且是esmodule的模块化方式。
用vue-cli创建一个项目,打开里面的main.js想要只要其中导入的vue的版本,需要在webpack的配置文件中查看,但是vue-cli对webpack做了深度的封装,所以在项目中找不到webpack的配置文件,但是vue-cli提供了一个命令行的工具,我们可以通过工具来查看webpack的配置vue inspact
,如果觉得查看不方便可以通过vue inspact > output.js
‘>’的作用是将前面命令运行的结果输出到目标文件
通过webpack配置可以看到,当我们在main.js里面导入的vue,导入的是runtime版本也就是运行时版本,因为运行时版本效率更高,比完整本少了3000多行代码,所推荐使用运行时版本。在开发项目的时候会有很多单文件组件,这些单文件组件浏览器是不支持的,所以在打包的时候会将这些单文件组件转换成js对象,在转换成js对象的过程中,他还会帮我们把模版template转换成render函数,所以单文件组件在运行的时候也是不需要编译器的。
- ESM格式被设计为可以被静态分析(也就是在编译去处理打包过程中的依赖,而不是运行的时候),所以打包工具可以利用这一点来‘tree-shaking’并将用不到的代码排除出最终的包(tree-shaking将项目中使用不到的代码剔除掉)
源码总结
vue首次渲染流程
总结:
- Vue初始化。初始化vue的实例成员和静态成员
- Vue实例化。调用vue的构造函数
- 在构造函数中调用vue._init()。初始化vm的生命周期相关变量( c h i l d r e n , children, children,parent, r o o t , root, root,ref),初始化vm的事件监听,初始化vm的编译render,触发beforeCreate,把inject成员注入到vm上,初始化vm的_props/methods/_data/computed/watch,初始化provide,触发created.
- 调用 m o u n t ( ) 。 存 在 两 个 mount()。存在两个 mount()。存在两个mount(),一个是在完整版本会调用的,作用是如果render选项不存在的话,将template编译成render函数,如果template选项也不存在的话,会把el中的内容作为模版,然后编译成render函数。另一个是完整版本和运行版本都会调用的,在这里会重新获取el,因为运行时版本是不会执行上一个$mount()的
- 调用mountComponent(),
- 判断是否有render选项,如果没有但是传入了template选项,在开发环境会发送警告
- 首先触发beforeMount钩子函数。
- 然后定义了updateComponent(),作用是调用_render()生成虚拟DOM,然后调用_update()将_render()生成的虚拟DOM转化成真实dom并且挂载在页面上。
- 创建Watcher实例,将刚刚定义的updateComponent()传入。创建完Watcher会调用一次get(),在get方法中会调用updateComponent()生成虚拟dom并且将虚拟dom转换成真实dom挂载在vm.$el中
- 触发mounted钩子函数
- 挂载结束,返回vue实例
四个导出vue的模块
- src/platforms/web/entry-runtime-with-compiler.js(核心作用:增加了编译的功能)
- web 平台相关的入口
- 重写了平台相关的 $mount() 方法(内部编译template模版,就是把template模版转换成render函数)
- 注册了 Vuepile() 方法,传递一个 HTML 字符串返回 render 函数
- src/platforms/web/runtime/index.js
- web 平台相关
- 注册和平台相关的全局指令:v-model、v-show
- 注册和平台相关的全局组件: v-transition、v-transition-group
- 全局方法:
- patch:把虚拟 DOM 转换成真实 DOM
- $ mount:挂载方法(把dom渲染到界面上去,这里定义了$ mount,在入口文件中重写了$mount)
- src/core/index.js
- 与平台无关
- 设置了 Vue 的静态方法,initGlobalAPI(Vue)(在这个方法中给vue增加了vue.set,vue.delete,vue.nextick等静态方法)
- src/core/instance/index.js(根vue实例相关的)
- 与平台无关
- 定义了构造函数,调用了 this._init(options) 方法(整个函数的入口,之前模拟过)
- 给 Vue 中混入了常用的实例成员(如 d a t a , data, data,maps,$set等实例成员)
vue 构造函数
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)
}
// 初始化vue _init方法
initMixin(Vue)
// 初始化data, prop watch等
stateMixin(Vue)
// 初始化发布-订阅事件模型
eventsMixin(Vue)
// 初始化组件生命周期钩子
lifecycleMixin(Vue)
// 初始化nextTick, render
renderMixin(Vue)
vue静态方法
- 在global-api/index.js里面初始化Vue的静态方法。
export function initGlobalAPI (Vue: GlobalAPI) { // config const configDef = {} configDef.get = () => config if (process.env.NODE_ENV !== 'production') { configDef.set = () => { warn( 'Do not replace the Vue.config object, set individual fields instead.' ) } } // 初始化 Vue.config 对象 Object.defineProperty(Vue, 'config', configDef) // exposed util methods. // NOTE: these are not considered part of the public API - avoid relying on // them unless you are aware of the risk. // 这些工具方法不视作全局API的一部分,除非你已经意识到某些风险,否则不要去依赖他们 Vue.util = { warn, extend, mergeOptions, defineReactive } // 静态方法 set/delete/nextTick Vue.set = set Vue.delete = del Vue.nextTick = nextTick // 2.6 explicit observable API // 让一个对象可响应 Vue.observable = <T>(obj: T): T => { observe(obj) return obj } // 初始化 Vue.options 对象,并给其扩展 // components/directives/filters Vue.options = Object.create(null) ASSET_TYPES.forEach(type => { Vue.options[type + 's'] = Object.create(null) }) // this is used to identify the "base" constructor to extend all plain-object // components with in Weex's multi-instance scenarios. Vue.options._base = Vue // 设置 keep-alive 组件 extend(Vue.options.components, builtInComponents) // 注册 Vue.use() 用来注册插件 initUse(Vue) // 注册 Vue.mixin() 实现混入 initMixin(Vue) // 注册 Vue.extend() 基于传入的options返回一个组件的构造函数 initExtend(Vue) // 注册 Vue.directive()、 Vueponent()、Vue.filter() initAssetRegisters(Vue) }
数据响应式原理
Vue响应式原理总结:
- 当vue实例被创建时,vue会遍历data选项的属性,用Object.defineProperty拦截并监听data中的属性成员,在其内部定义getter,在属性被访问时进行收集依赖(把属性对应的watcher对象添加到dep的subs数组中),定义setter在属性被修改时,进行派发更新(调用dep.notify()发送通知,它内部会调用Watcher对象的update方法更新视图)。每个组件实例都有相应的Watcher程序实例,他会在属性渲染的过程中把属性记录为依赖,之后当依赖项被setter调用的时候,会通知Watcher重新计算,从而使得他关联的组件得到更新。
- 在数据的响应化处理的时候,由于object.defineProperty监听不到数组的一些原生方法,所以在observer中对数组的push,pop,sort,splice,shift,unshift,reverse几种原生方法做了响应式处理,所以在这些方法被调用的时候需要发送通知,发送通知的时候找到数组对象对应的observer对象,调用其中的dep中的notify方法派发更新。
- 对象的响应式处理,调用walk方法遍历这个对象中的所有属性,对每一个属性调用defineReactive进行响应式处理
- 在挂载dom的时候mountComponent方法中创建Wacther实例,并调用get方法,在get方法中调用pushTarget记录Dep.target也就是当前的watcher对象,当访问对象属性的时候,会触发getter进行依赖收集,把属性对应的Watcher对象添加到subs数组中,如果属性的值也是对象,则会创建childOb,为子对象收集依赖
- 当修改属性值的时候会触发setter方法,在其中会dep.notify()会调用subs数组中的每个Watcher对象的update方法,update方法会调用queueWatcher将当前Watcher添加到队列中,然后队列进行排序执行,调用Watcher.run更新视图,最后清空依赖,重置watcher中的状态。
set函数
原理:他会去处理数组的响应式和对象的响应式,当使用set给数组设置值的时候,会调用splice方法,当使用set给对象增加新的成员的时候,会调用defineReactive,把key设置为响应式属性,最终会调用ob的dep对象的notify方法发送通知
nextTick
Vue.nextTick用于延迟执行一段代码,它接受2个参数(回调函数和执行回调函数的上下文环境),如果没有提供回调函数,那么将返回promise对象
虚拟DOM
什么是虚拟DOM
虚拟 DOM(Virtual DOM) 是使用 JavaScript 对象来描述 DOM,虚拟 DOM 的本质就是 JavaScript 对
象,使用 JavaScript 对象来描述 DOM 的结构。应用的各种状态变化首先作用于虚拟 DOM,最终映射
到 DOM。Vue.js 中的虚拟 DOM 借鉴了 Snabbdom,并添加了一些 Vue.js 中的特性,例如:指令和组
件机制。
为什么要使用虚拟DOM
- 使用虚拟 DOM,可以避免用户直接操作 DOM,开发过程关注在业务代码的实现,不需要关注如
何操作 DOM,从而提高开发效率 - 作为一个中间层可以跨平台,除了 Web 平台外,还支持 SSR、Weex。
- 关于性能方面,在首次渲染的时候肯定不如直接操作 DOM,因为要维护一层额外的虚拟 DOM,
如果后续有频繁操作 DOM 的操作,这个时候可能会有性能的提升,虚拟 DOM 在更新真实 DOM
之前会通过 Diff 算法对比新旧两个虚拟 DOM 树的差异,最终把差异更新到真实 DOM
在虚拟dom中使用key的好处: - 作用: 在sameVnode函数中用于比较新旧Vnode是否相同,如果key相同且tag相同且都不为注释节点,则视为相同,继续调用patchVnode做进一步对比,如果不相同则删除旧Vnode,添加新Vnode
- 好处:他可以快速对比两个虚拟dom是否相同,从而复用当前元素,如果没有key值的话,就会根据就地复用原则,一个一个对比,然后修改渲染,如果key值是index的话,假如数组中间插入一项,此时从这一项开始的key值就全部都变了,都需要重新渲染。如果有key值且不是index的话,diff算法就可以通过对比找到正确的位置插入新节点,而key值相同的dom节点就不需要去比较了。这样就可以减少dom操作,也可以减少diff算法执行和dom渲染所需的时间,提升了性能。
h函数
- vm.$createElement(tag,data,children,normalizeChildren)
- tag 标签名称或者组件对象
- data 描述tag,可以设置DOM的属性或者标签的属性
- children tag中的文本内容或者子节点
- VNode的核心属性
- tag (调用h函数传入的tag)
- data
- children
- text
- elm (记录真实dom)
- key (复用当前元素)
模板编译
总结
- 模版编译就是将template模版转换为render函数。
- 首先从缓存中加载编译好的render函数,如果有直接返回
- 如果缓存中没有,则开始调用baseCompile()开始编译。
- 通过parse函数将模版转换成AST对象,且模版中的属性和指令都会记录在ast对象的相应属性上
- 通过optimize函数对生成的AST对象进行优化,检测并标记其中的静态子节点和静态根节点,这一步主要是用来做虚拟dom的渲染优化,因为标记之后的节点在patch的过程中会被跳过,不需要每一次重新渲染。
- 通过generate函数将优化好的AST对象转换成字符串形式的js代码
- 把字符串形式的js代码通过new function转换成匿名函数,这个匿名函数就是最后生成的render函数
- 将render函数挂载到option中
- 执行公共的mount函数
- 通过查看模版编译的源代码可以了解到模版编译的过程中会标记静态根节点,对静态根节点进行优化处理,重新渲染的时候不需要再次处理静态根节点,因为他的内容不会发生改变,另外在模版中不要写过多的无意义的换行。否则生成对应的ast对象会保留这些空白和换行,他们都会被存储到内存中,而这些空白和换行对浏览器渲染来说是没有意义的。
组件化
- 一个vue组件就是一个拥有预定义选项的一个vue实例
- 一个组件可以组成页面上一个功能完备的区域,组件可以包含脚本、样式、模版
- 组件化可以让我们方便的把页面拆分成多个可重用的组件,使用组件可以让我们重用页面中的某一个区域,组件之间也是可以嵌套的。
- 组件的创建过程是先创建父组件再创建子组件,组件的挂载过程是先挂载子组件再挂载父组件
- 组件的粒度不是越小越好,因为嵌套一层组件就要重复执行一遍组件的创建过程,比较消耗性能,组件的抽象过程要合理
关于Vue源码的总结
经过这一次对vue源码的学习,从一开始的完全看不进去到后来有一点点头绪,到现在基本理出一些思路,其实看源码还是要带着目的看的,这样才会有收获也会更有动力,总结了一些从源码中得到的收获。
-
源码中封装了很多常用的函数(如:类型判断,类型转换等)
function isObject(obj) { return obj !== null && typeof obj === 'object'} function isUndef(v) { return v === undefined || v === null} function isDef(v) { return v !== undefined && v !== null} function toString(val) { return val == null ? '' : typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val) }
这样做有利于后期对这些常用函数的维护
-
用到了一些函数式编程的思想
比如在虚拟dom那块的createPatchFunction的最后返回了patch函数,在返回之前已经初始化好了和patch相关的modules和nodeOps这两个数据,所以createPatchFunction其实也是柯里化函数。还有在模版编译那块使用了大量的闭包层层嵌套 -
使用了很多标志符
- isStatic:是否是静态节点
- isComment: 是否是注释节点
- isComponent: 是否是组件
- isMounted: dom是否已经挂载
标志符可以代替多余的判断,控制流程,减少开销
-
vue本身其实就是一个构造函数
-
在webpack中vue-loader插件做的事情是:
- 对 *.vue 取每一个语言块,组装到CommonJS模块。
- template -> 默认使用html,每个vue文件只能包含一个template模块,内容作为字符串提取,然后将提取出来的字符串编译成render函数,交给vue实例去渲染
- script -> 默认使用JS,如果检测到(babel-loader,会自动支持ES2015),最多只能有一个script模块,打包之后就是在js的环境下执行
- style -> 默认使用css,可以支持多个style标签,选择使用scoped或者module属性,封装当前的组件样式,通常被style-loader提取。sroped中的css只应用于当前的组件元素中
- 资源url处理:在默认情况下,vue-loader会自动处理样式和模板文件。例如,url(./ image.png)将被翻译为require(’./ image.png’)
-
最后还想说一下对于vue渐进式的理解:
- 官网:Vue是一套用于构建用户界面的渐进式框架
- 在我们使用vue时,一般的流程就是:声明式渲染=》组件化应用=》客户端路由=》集中式状态管理=》项目构建
- 声明式渲染基本上就是上面总结的首次渲染中的数据响应式处理,生成虚拟dom并转换成真实dom以及模版编译
- 组件化应用就是每一个声明式的文件都可以当成一个组件,一个vue组件就是一个拥有预定义选项的一个vue实例
- 客户端路由就是通过路由的特性实现单页面应用
- 集中式状态管理就是vuex的应用
- 项目构建就是在大型项目中由开发,打包,部署,上线等流程
- 所以从vue的使用方式上来进行区分的,可以只进行声明式渲染就是上面说的用于构建用户界面的模版引擎,也可以使用更多vue底层的服务。作者只提供当前框架的核心功能,并提供一个采用prototype的形式去挂载其他插件(可以是纯粹的方法,也可以是混入的钩子函数,vue会去监听并记录所有的钩子函数)
- 最后引入别人对于渐进式的总结
更多推荐
Vue源码解析
发布评论