You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 本质上就是一个用 Function 实现的 Class,然后在它的原型 prototype 以及它本身上扩展了一系列的方法和属性。
Vue 支持 2 种事件类型,原生 DOM 事件和自定义事件,它们主要的区别在于添加和删除事件的方式不一样,并且自定义事件的派发是往当前实例上派发,但是可以利用在父组件环境定义回调函数来实现父子组件的通讯。另外要注意一点,只有组件节点才可以添加自定义事件,并且添加原生 DOM 事件需要使用 native 修饰符;而普通元素使用 .native 修饰符是没有作用的,也只能添加原生 DOM 事件。
2. v-model
实现
在理解 Vue 的时候都把 Vue 的数据响应原理理解为双向绑定,但实际上这是不准确的,我们之前提到的数据响应,都是通过数据的改变去驱动 DOM 视图的变化,而双向绑定除了数据驱动 DOM 外, DOM 的变化反过来影响数据,是一个双向关系,在 Vue 中,我们可以通过 v-model 来实现双向绑定。
表单元素
结合示例来分析:
let vm = new Vue({
el: '#app',
template: '<div>'
+ '<input v-model="message" placeholder="edit me">' +
'<p>Message is: {{ message }}</p>' +
'</div>',
data() {
return {
message: ''
}
}
})
前言
本文内容讲解的内容:一张思维导图辅助你深入了解 Vue | Vue-Router | Vuex 源码架构。
项目地址:https://github.com/biaochenxuying/vue-family-mindmap
markdown 文字版
pdf 版
先来张 Vue 全家桶 总图:
1. 项目目录
scripts: 构建相关的文件,一般情况下我们不需要动。
dist: 构建后文件的输出目录
examples: 存放一些使用Vue开发的应用案例
flow: 类型声明,使用开源项目 [Flow]
packages: 存放独立发布的包的目录
test: 包含所有测试文件
src: 源码,重点
compiler: 编译器代码的存放目录,将 template 编译为 render 函数
core: 核心代码 ,与平台无关的代码
platforms: 不同平台的支持,包含平台特有的相关代码,不同平台的不同构建的入口文件也在这里
web:web平台
template
选项,我们使用vue默认导出的就是这个运行时的版本。大家使用的时候要注意weex:混合应用
serve: 服务端渲染,包含(server-side rendering)的相关代码
sfc: 包含单文件组件( .vue 文件)的解析逻辑,用于vue-template-compiler包
shared: 共享代码,包含整个代码库通用的代码
package.json:对项目的描述文件,包含了依赖包等信息
yarn.lock :yarn 锁定文件
.editorconfig:针对编辑器的编码风格配置文件
.flowconfig:flow 的配置文件
.babelrc:babel 配置文件
.eslintrc:eslint 配置文件
.eslintignore:eslint 忽略配置
.gitignore:git 忽略配置
2. 源码构建,基于 Rollup
1. 根据 format 构建格式可分为三个版(再根据有无 compiler ,每个版本中又可以再分出二个版本)
cjs:表示构建出来的文件遵循 CommonJS 规范
es:构建出来的文件遵循 ES Module 规范
umd:构建出来的文件遵循 UMD 规范
2. 总结
Runtime + Compiler:我们如果没有对代码做预编译,但又使用了 Vue 的 template 属性并传入一个字符串,则需要在客户端编译模板。Vue.js 2.0 中,最终渲染都是通过 render 函数,如果写 template 属性,则需要编译成 render 函数,那么这个编译过程会发生运行时,所以需要带有编译器的版本。
3. vue 本质:构造函数
总结
4. 数据驱动
1. new Vue
2. init
调用 this._init(options) 进行初始化
3. Vue 实例挂载 $mount
$mount 这个方法的实现是和平台、构建方式都相关的。我们分析带 compiler 版本的 $mount 实现。在 Vue 2.0 版本中,所有 Vue 的组件最终都会转换成 render 方法。
4. compile
5. render: Vue 的 _render 方法是实例的一个私有方法,最终会把实例渲染成一个虚拟 Node。
6. Virtual DOM(虚拟 dom): 本质上是一个原生的 JS 对象,用 class 来定义。
规范化 children 后,会去创建一个 VNode 的实例。
7. update 的核心:调用 vm.patch 方法
update:通过 Vue 的 _update 方法,_update 方法的作用是把 VNode 渲染成真实的 DOM。_update 的核心就是调用 vm.patch 方法,__patch__在不同的平台,比如 web 和 weex 上的定义是不一样的。
8. DOM:Vue 最终创建的 DOM。
9. 总结
new Vue => init => $mounted => compile => render => vnode => patch => DOM
5. 组件化
1. introduction
2. createComponent
在 createElement 的实现的时候,如果不是一个普通的 html 标签,就是通过 createComponent 方法创建一个组件 VNode。
Vue.extend 函数
Vue.extend 的作用就是构造一个 Vue 的子类,它使用一种非常经典的原型继承的方式把一个纯对象转换一个继承于 Vue 的构造器 Sub 并返回,然后对 Sub 这个对象本身扩展了一些属性,如扩展 options、添加全局 API 等;并且对配置中的 props 和 computed 做了初始化工作;最后对于这个 Sub 构造函数做了缓存,避免多次执行 Vue.extend 的时候对同一个子组件重复构造。
安装组件钩子函数:installComponentHooks(data)
3. path
4. 合并配置
5. 生命周期
6. 组件注册
7. 异步组件
代码
代码
代码
异步组件实现的本质是 2 次渲染,除了 0 delay 的高级异步组件第一次直接渲染成 loading 组件外,其它都是第一次渲染生成一个注释节点,当异步获取组件成功后,再通过 forceRender 强制重新渲染,这样就能正确渲染出我们异步加载的组件了。
6. 深入响应式原理
1. 响应式对象:Vue.js 实现响应式的核心是利用了 ES5 的 Object.defineProperty。
直接在一个对象上定义一个新属性,或者修改一个对象的现有属性
主要是对 props、methods、data、computed 和 wathcer 等属性做了初始化操作
proxy 方法的实现很简单,通过 Object.defineProperty 把 target[sourceKey][key] 的读写变成了对 target[key] 的读写。
2. 依赖收集:响应式对象 getter 相关的逻辑就是做依赖收集
Watcher 是一个 Class,定义了一些和 Dep 相关的属性, 还定义了一些原型的方法,和依赖收集相关的有 get、addDep 和 cleanupDeps 方法。
总结
3. 派发更新
修改值的时候,会触发 setter ,会对新设置的值变成一个响应式对象,并通过 dep.notify() 通知所有的订阅者
做派发更新的时候的一个优化的点,它并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列里,然后在 nextTick 后执行 flushSchedulerQueue
总结
4. nextTick
JS 执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤
主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。
vue 中 nextTick 实现
这里使用 callbacks 而不是直接在 nextTick 中执行回调函数的原因是保证在同一个 tick 内多次执行 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。
对 nextTick 的分析,并结合上一节的 setter 分析,我们了解到数据的变化到 DOM 的重新渲染是一个异步过程,发生在下一个 tick。这就是我们平时在开发的过程中,比如从服务端接口去获取数据的时候,数据做了修改,如果我们的某些方法去依赖了数据修改后的 DOM 变化,我们就必须在 nextTick 后执行。
Vue.js 提供了 2 种调用 nextTick 的方式,一种是全局 API Vue.nextTick,一种是实例上的方法 vm.$nextTick,无论我们使用哪一种,最后都是调用 next-tick.js 中实现的 nextTick 方法。
5. 检测变化的注意事项
对于使用 Object.defineProperty 实现响应式的对象,当我们去给这个对象添加一个新的属性的时候,是不能够触发它的 setter 的
要用 Vue.set 方法
set 方法是在对象上设置属性。添加新属性和如果属性不存在,通过 defineReactive(ob.value, key, val) 把新添加的属性变成响应式对象,然后再通过 ob.dep.notify() 手动的触发依赖通知。
Vue 也是不能检测到以下变动的数组
总结
6. 计算属性 VS 侦听属性
计算属性本质上就是一个 computed watcher,确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher 重新渲染。
本质上侦听属性也是基于 Watcher 实现的,它是一个 user watcher
deep watcher
user watcher
computed watcher
sync watcher
7. 组件更新:过程的核心就是新旧 vnode diff,对新旧节点相同以及不同的情况分别做不同的处理。
新旧 vnode 不同,本质上是要替换已存在的节点。
7. 编译
1. introduction
2. 编译入口
mount 的时候,通过 compileToFunctions 方法就是把模板 template 编译生成 render 以及 staticRenderFns
3. parse
编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。
整体流程
整体来说它的逻辑就是循环解析 template ,用正则做各种匹配,对于不同情况分别进行不同的处理,直到整个 template 被解析完毕。 在匹配的过程中会利用 advance 函数不断前进整个模板字符串,直到字符串末尾。
在处理开始标签的时候为每一个标签创建了一个 AST 元素,在不断解析模板创建 AST 元素的时候,我们也要为它们建立父子关系,就像 DOM 元素的父子关系那样。
end () {
treeManagement()
closeElement()
}
chars (text: string) {
handleText()
createChildrenASTOfText()
}
4. optimize
当我们的模板 template 经过 parse 过程后,会输出生成 AST 树,那么接下来我们需要对这颗树做优化,Vue 是数据驱动,是响应式的,但是我们的模板并不是所有数据都是响应式的,也有很多数据是首次渲染后就永远不会变化的,那么这部分数据生成的 DOM 也不会变化,我们可以在 patch 的过程跳过对他们的比对。
总结
5. codegen
例子:
8. 扩展
1. event
Vue 还支持了自定义事件,并且自定义事件只能作用在组件上,如果在组件上使用原生事件,需要加 .native 修饰符,普通元素上使用 .native 修饰符无效
总结
2. v-model
实现
在理解 Vue 的时候都把 Vue 的数据响应原理理解为双向绑定,但实际上这是不准确的,我们之前提到的数据响应,都是通过数据的改变去驱动 DOM 视图的变化,而双向绑定除了数据驱动 DOM 外, DOM 的变化反过来影响数据,是一个双向关系,在 Vue 中,我们可以通过 v-model 来实现双向绑定。
结合示例来分析:
这实际上就是 input 实现 v-model 的精髓,通过修改 AST 元素,给 el 添加一个 prop,相当于我们在 input 上动态绑定了 value,又给 el 添加了事件处理,相当于在 input 上绑定了 input 事件,其实转换成模板如下:
其实就是动态绑定了 input 的 value 指向了 messgae 变量,并且在触发 input 事件的时候去动态把 message 设置为目标值,这样实际上就完成了数据双向绑定了,所以说 v-model 实际上就是语法糖。
通过一个例子分析:
可以看到,父组件引用 child 子组件的地方使用了 v-model 关联了数据 message;而子组件定义了一个 value 的 prop,并且在 input 事件的回调函数中,通过 this.$emit('input', e.target.value) 派发了一个事件,为了让 v-model 生效,这两点是必须的。
其实就相当于我们在这样编写父组件:
子组件传递的 value 绑定到当前父组件的 message,同时监听自定义 input 事件,当子组件派发 input 事件的时候,父组件会在事件回调函数中修改 message 的值,同时 value 也会发生变化,子组件的 input 值被更新。
总结
3. slot
总结
4. keep-alive
5. transition
总结
6. transition-group
9. Vue-Router
1. introduction
2. 路由注册
3. VueRouter 对象
4. matcher
addRoutes 方法的作用是动态添加路由配置,因为在实际开发中有些场景是不能提前把路由写死的,需要根据一些条件动态添加路由
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
match 会根据传入的位置和路径计算出新的位置并匹配到相应的路由 record ,然后根据新的位置 和 record 创建新的路径并返回。
5. 路径切换
实际上就是发生在路由路径切换的时候,执行的一系列钩子函数。
完整的导航解析流程
路由最终的渲染离不开组件,Vue-Router 内置了 组件。 是一个 functional 组件,它的渲染也是依赖 render 函数。
在混入的 beforeCreate 钩子函数中,会执行 registerInstance 方法,进而执行 render 函数中定义的 registerRouteInstance 方法,从而给 matched.instances[name] 赋值当前组件的 vm 实例。
return h(component, data, children)
当我们执行 transitionTo 来更改路由线路后,组件是如何重新渲染 ?
在 Vue 混入的 beforeCreate 钩子函数中,我们把根 Vue 实例的 _route 属性定义成响应式的了。
Vue-Router 还内置了另一个组件 ,它支持用户在具有路由功能的应用中(点击)导航。 通过 to 属性指定目标地址,默认渲染成带有正确链接的 标签,可以通过配置 tag 属性生成别的标签。另外,当目标路由成功激活时,链接元素自动设置一个表示激活的 CSS 类名。
实际上就是执行了 history 的 push 和 replace 方法做路由跳转。
6. 总结
10. Vuex
1. introduction
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
什么是“状态管理模式”?
Vuex 核心思想
2. Vuex 初始化
它其实给 Vue 全局混入了一个 beforeCreate 钩子函数,它的实现非常简单,就是把 options.store 保存在所有组件的 this.$store 中,这个 options.store 就是我们在实例化 Store 对象的实例。
Store 对象的构造函数也是一个 class,接收一个对象参数,它包含 actions、getters、state、mutations、modules 等 Vuex 的核心概念
初始化模块
Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter,甚至是嵌套子模块——从上至下进行同样方式的分割
从数据结构上来看,模块的设计就是一个树型结构,store 本身可以理解为一个 root module,它下面的 modules 就是子模块,Vuex 需要完成这颗树的构建。
调用 register 方法,通过 const newModule = new Module(rawModule, runtime) 创建了一个 Module 的实例,Module 是用来描述单个模块的类。
register 首先根据路径获取到父模块,然后再调用父模块的 addChild 方法建立父子关系。
register 的最后一步,就是遍历当前模块定义中的所有 modules,根据 key 作为 path,递归调用 register 方法,这样就建立父子关系。
对模块中的 state、getters、mutations、actions 做初始化工作
它的入口代码是:
默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。如果我们希望模块具有更高的封装度和复用性,可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
构造了一个本地上下文环境:
总结: 所以 installModule 实际上就是完成了模块下的 state、getters、actions、mutations 的初始化工作,并且通过递归遍历的方式,就完成了所有子模块的安装工作。
Store 实例化的最后一步,就是执行初始化 store._vm 的逻辑,它的入口代码是:
resetStoreVM 的作用实际上是想建立 getters 和 state 的联系,因为从设计上 getters 的获取就依赖了 state ,并且希望它的依赖能被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。因此这里利用了 Vue 中用 computed 计算属性来实现。
strict mode
当严格模式下,store._vm 会添加一个 wathcer 来观测 this._data.$$state 的变化,也就是当 store.state 被修改的时候, store._committing 必须为 true,否则在开发阶段会报警告。
我们要把 store 想象成一个数据仓库,为了更方便的管理仓库,我们把一个大的 store 拆成一些 modules,整个 modules 是一个树型结构。每个 module 又分别定义了 state,getters,mutations、actions,我们也通过递归遍历模块的方式都完成了它们的初始化。为了 module 具有更高的封装度和复用性,还定义了 namespace 的概念。最后我们还定义了一个内部的 Vue 实例,用来建立 state 到 getters 的联系,并且可以在严格模式下监测 state 的变化是不是来自外部,确保改变 state 的唯一途径就是显式地提交 mutation。
3. API
Vuex 最终存储的数据是在 state 上的,我们之前分析过在 store.state 存储的是 root state,那么对于模块上的 state,假设我们有 2 个嵌套的 modules,它们的 key 分别为 a 和 b,我们可以通过 store.state.a.b.xxx 的方式去获取。
action 类似于 mutation,不同在于 action 提交的是 mutation,而不是直接操作 state,并且它可以包含任意异步操作。
mapState 支持传入 namespace, 因此我们可以这么写:
在 mapState 的实现中,如果有 namespace,则尝试去通过 getModuleByNamespace(this.$store, 'mapState', namespace) 对应的 module,然后把 state 和 getters 修改为 module 对应的 state 和 getters
主要原因是在 Vuex 初始化执行 installModule 的过程中,初始化了这个映射表:
mapGetters 的用法:
和 mapState 类似,mapGetters 是将 store 中的 getter 映射到局部计算属性
mapGetters 也同样支持 namespace,如果不写 namespace ,访问一个子 module 的属性需要写很长的 key,一旦我们使用了 namespace,就可以方便我们的书写,每个 mappedGetter 的实现实际上就是取 this.$store.getters[val]。
我们可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 的调用。mapMutations 支持传入一个数组或者一个对象,目标都是组件中对应的 methods 映射为 store.commit 的调用。
mapMutations 的用法:
mappedMutation 同样支持了 namespace,并且支持了传入额外的参数 args,作为提交 mutation 的 payload,最终就是执行了 store.commit 方法,并且这个 commit 会根据传入的 namespace 映射到对应 module 的 commit 上。
在组件中使用 this.$store.dispatch('xxx') 提交 action,或者使用 mapActions 辅助函数将组件中的 methods 映射为 store.dispatch 的调用。
mapActions 在用法上和 mapMutations 几乎一样,实现也很类似,和 mapMutations 的实现几乎一样,不同的是把 commit 方法换成了 dispatch。
在有一些场景下,我们需要动态去注入一些新的模块,Vuex 提供了模块动态注册功能,在 store 上提供了一个 registerModule 的 API。
registerModule 支持传入一个 path 模块路径 和 rawModule 模块定义,首先执行 register 方法扩展我们的模块树,接着执行 installModule 去安装模块,最后执行 resetStoreVM 重新实例化 store._vm,并销毁旧的 store._vm。
相对的,有动态注册模块的需求就有动态卸载模块的需求,Vuex 提供了模块动态卸载功能,在 store 上提供了一个 unregisterModule 的 API。
4. 插件
Vuex 除了提供的存取能力,还提供了一种插件能力,让我们可以监控 store 的变化过程来做一些事情。
Logger 函数,它相当于订阅了 mutation 的提交,它的 prevState 表示之前的 state,nextState 表示提交 mutation 后的 state,这两个 state 都需要执行 deepCopy 方法拷贝一份对象的副本,这样对他们的修改就不会影响原始 store.state。
接下来就构造一些格式化的消息,打印出一些时间消息 message, 之前的状态 prevState,对应的 mutation 操作 formattedMutation 以及下一个状态 nextState。
最后更新 prevState = nextState,为下一次提交 mutation 输出日志做准备。
总结
11. 已完成与待完成
已完成:
待完成:
因为该项目都是业余时间做的,笔者能力与时间也有限,很多细节还没有完善。
如果你是大神,或者对 vue 源码有更好的见解,欢迎提交 issue ,大家一起交流学习,一起打造一个像样的 讲解 Vue 全家桶源码架构 的开源项目。
12. 总结
以上内容是笔者最近学习 Vue 源码时的收获与所做的笔记,本文内容大多是开源项目 Vue.js 技术揭秘 的内容,只不过是以思维导图的形式来展现,内容有省略,还加入了笔者的一点理解。
笔者之所以采用思维导图的形式来记录所学内容,是因为思维导图更能反映知识体系与结构,更能使人形成完整的知识架构,知识一旦形成一个体系,就会容易理解和不易忘记。
13. 最后
如果你觉得本文章或者项目对你有启发,请给个赞或者 star 吧,点赞是一种美德,谢谢。
参考开源项目:
The text was updated successfully, but these errors were encountered: