blog's People
blog's Issues
webpack 分离 chunk
我们使用webpack
打包应用时,默认情况下一个入口最终产生一个JS文件。在使用vue-cli
进行打包时,除了app.js
之外,还有vendor.js
。
还有很常见的一种打包方式会将JS文件分为3个部分。
- app.js 业务代码
- vendor.js 第三方库代码
- manifest.js
- webpack 的初始化代码
- 参考腾讯前端团队写的Webpack原理-输出文件分析
这样做有什么好处呢?
在生产环境中,一般会为静态资源加上文件指纹。假如业务代码和第三方库代码打包到一起,那么任何一点代码的改动都会让文件指纹发生改变。
第三方库代码一般是不会改变的,假如将它单独提取出来,打包成一个文件,在库代码没有发生变化的情况下文件指纹就不会修改。这样做可以更好的利用客户端的缓存能力,减少请求次数。
抽取模块
在webpack3中,使用CommonsChunkPlugin
在webapck4中,使用的是SplitChunkPlugin
环境
目前webpack的版本为4.40.2
。使用CommonsChunkPlugin
时,将webpack的版本修改为3.3.0
// 项目结构
├── app.js
├── package.json
├── util.js
└── webpack.config.js
// app.js
import react
console.log('app.js')
// util.js
export function foo() {
console.log('util.js')
}
CommonsChunkPlugin
下面是webpack的配置文件
// webpack.config.js
const webpack = require('webpack')
const path = require('path')
module.exports = {
entry: {
app: './app',
// vendor 这个 Chunk 只包含了 react,而 app.js 中也同样使用了 react ,因此插件可以从中抽取出
// 公共的部分, 也就是 react
vendor: ['react']
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[chunkhash].js',
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function(module){
return module.context && module.context.includes('node_modules');
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
],
};
运行一下npx webpack --config webapck.config.js
进行打包
第一次打包结果
Hash: bf0ff02fe4d883a5152e
Version: webpack 3.3.0
Time: 145ms
Asset Size Chunks Chunk Names
vendor.6d6ff8a174ebb5c83bba.js 93.8 kB 0 [emitted] vendor
app.dde83d0702db93ce46d5.js 450 bytes 1 [emitted] app
manifest.5d0c9ed65d61958411f2.js 5.85 kB 2 [emitted] manifest
[3] ./app.js 40 bytes {1} [built]
[8] multi react 28 bytes {0} [built]
+ 7 hidden modules
出现了三个文件,没有问题。
//app.js
import 'react'
import './util.js'
修改一下app.js
,引入其他业务模块,再一次打包
第二次打包结果
Hash: d4cc66daafa987cbb978
Version: webpack 3.3.0
Time: 146ms
Asset Size Chunks Chunk Names
vendor.8d03c7d7bd91cf3f7ddc.js 93.8 kB 0 [emitted] vendor
app.1b91e0fcccbac051e6a1.js 725 bytes 1 [emitted] app
manifest.368326d1543c92ebcaf9.js 5.85 kB 2 [emitted] manifest
[3] ./app.js 59 bytes {1} [built]
[8] ./util.js 52 bytes {1} [built]
[9] multi react 28 bytes {0} [built]
+ 7 hidden modules
值得注意的是,第三方库没有变化,可是vendor
的指纹改变了。
在网上查了挺多相关配置,最终还是在官方文档中找到答案。
// vendor.js
// 打开 vendor.js 两次打包中,id 发生了变化,导致 hash 改变
webpackJsonP([id1], [(function() {...}, ...)], [id2])
原因是,打包的过程中,webpack
会给模块一个id
,通过这个id
来标识一个模块。当增加一个模块的时候,id
的解析发生变化了,因此生成的vendor
发生了变化。
官方给出的解决方案是,将id
改为哈希的。
于是搜了一下有没有能让模块ID改为哈希值的插件,果真有——HashedModuleIdsPlugin
,将这个插件加入到插件列表后确实达到了目的。这个hash值是根据什么来产生的呢?文档的开头也给了解释。
This plugin will cause hashes to be based on the relative path of the module, generating a four character string as the module id. Suggested for use in production.
根据模块的相对路径生成四个字符的哈希值,并且建议用在生产环境。
在webpack的插件中加入
new webpack.HashedModuleIdsPlugin()
两次打包的vendor指纹是不会发生改变的。
SplitChunkPlugin
在webpack4
中,已经不推荐使用CommonsChunkPlugin
了。这篇文章对比了一下两个插件,说了CommonsChunkPlguin
的一些缺点。
// webpack.config.js
module.exports = {
// ...
optimization: {
runtimeChunk: 'single',
moduleIds: 'hashed',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
};
SplitChunkPlugin
可以通过配置模块引用次数、模块大小、正则去很方便的分割出一个新的Chunk
用之前的方法打包看看。在这个过程中,我发现没加moduleIds: 'hashed'
这一条时,已经达到效果,好像webpack的文档和实现已经不太同步了。
在vendor
文件中,模块不再以数字的方式标识id
,而是改为文件的相对路径
比如 ./node_modules/[email protected]@object-assign/index.js
。
所以,当三方库的依赖没变化时,它的哈希值也不会发生改变。
总结
在学习webpack的过程中,要多动手实践。以后希望能腾出时间深入学习。
参考
vue-loader
思路
webpack 只能处理 JS 和 JSON 文件,其余的文件类型需要依靠 loader 处理。vue-loader 是怎么处理 vue 文件的呢?
参考 vue-loader
- 将 vue 文件中的
template
script
style
分为三个块 (block) 分别引入- 被 loader 处理过的文件,都会携带一些 query,进行标识
- 按照 webpack 中的配置处理特定的 block
- 比如配置中使用
sass-loader
css-loader
style-loader
处理 css,那么将使用这几个 loader 对style block
进行处理
- 比如配置中使用
// code returned from the main loader for 'source.vue'
// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'
script.render = render
export default script
实现
先单独了解 vue-loader 流程中一些模块的职责
plugin
使用 vue-loader 时,需要在 webpack 配置中添加 VueLoaderPlugin
- 克隆一份 webpack 配置中的 rule,增加 resouceQuery 来判断是否为 vue 生成的文件引入,vue-loader 处理过的文件都带有 vue query
- 生成 pitcher-loader
- 拦截所有带 vue query 的请求
- 获取当前请求匹配的 loader
- 生成 inline loader 请求
- 返回增加 1、2步后的 loader rules
vue-loader
普通的 vue 文件请求
如 import './source.vue'
普通的 vue 文件使用 @vue/component-compiler-utils
处理后,分割为三个块,分别是 template
style
script
然后,对这三个块单独进行请求
// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'
带 query 的 vue 文件请求
如 import render from 'source.vue?vue&type=template'
vue-loader 返回文件中的某个具体 block
pitcher-loader
根据 pitch 的特性,先会正向的执行 loader 中的 pitch 函数,如果 pitch 有返回值,就不会再执行剩余的 loader pitch 以及 loader 功能了。
在 plugin 中,生成了一个 pitcher,用于拦截所有带 vue query 的请求。然后用克隆的 rules 规则进行匹配,生成对应的 inline loader ,loader 将从右到左顺序执行
'-!../lib/loaders/templateLoader.js??vue-loader-options!../node_modules/[email protected]@pug-plain-loader/index.js!../lib/index.js??vue-loader-options!./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&'
整体流程
- 请求 vue 文件
- 转为三个带 query 的请求
- pitcher loader 拦截,并生成 inline loader
- inline loader 处理源文件
scoped 实现
通过文件内容以及路径哈希值生成 id
const id = hash(
isProduction
? (shortFilePath + '\n' + source)
: shortFilePath
)
在 query 中将 id 传递给 template 以及 style 块
template
将 scopeId 添加到 vue options 中
Vue({
render: ...,
_scopeId: 'data-v-xxxxxx'
})
vue 在 patch 过程中的 createElm
,调用 setScope(vnode)
,将其添加到 dom 节点上
style
stylePostLoader 中使用 postcss 对 css 进行处理,增加修饰
其他
注意到导出 template
模块为 render
函数,实际上 vue-loader
会用vue-template-compiler
将模板编译为渲染函数。
这意味着,如果我们使用 Vue SFC 的方式进行开发,编译过程实际上是 vue-loader
做的,因此我们只需要引入 Vue 的运行时版本,这也是 Vue 的默认引入方式。
我们只需要保证,入口文件中也使用 render
创建 Vue,而不是template
new Vue({
render: h => h(App)
})
vue事件绑定原理
这是一个简单的 vue demo。
let vue = new Vue({
el: '#app',
template: `
<div @click="handleClick('abcd')"></div>
`,
methods: {
handleClick (a) {
console.log(a)
}
}
})
从 Vue 的整个流程思考,看Vue是如何将事件进行绑定的。
- vue 初始化
- 模板编译
- patch
Vue初始化
vue 初始化 _init
函数中,会调用 initEvents
初始化事件相关的动作。
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
每一个 vue 实例,创建了一个 _event
对象,这个对象实际上是给虚拟事件用的,并不是真实的 DOM 事件,使用$on
在对象中添加事件,$emit
进行触发。紧跟着,从options
中拿_parentListeners
,然后进行更新。
由于我们当前例子只会产生一个 vue 实例,先暂时忽略 _parentListeners
。
模板编译
由于我们给的是 template,vue 会将模板编译,产出 AST 和 render 函数
ast
// 生成的 AST 对象
{
attrsMap: { @click: "handleClick('abcd')"},
events: {
'click': {
value: "handleClick('abcd')",
}
}
}
模板编译后得到抽象语法树,树里包含了实例化一个真实节点的所有信息。比如当前 element 的属性,子节点 children 等等。这里只截取了一部分属性。
在 attrsMap 中可以发现,我们的事件和 style、class 这些真实属性没有区别,只是=
分割开来,前面是 key,后面是 value。
因为对 html-parse 阶段来说,@click="handleclick('abcd')
与 class="a b"
是没有区别的,都只是 html 中的属性,只是 vue 需要对这种属性做特殊的处理。
vue 通过正则/^@|^v-on:/
判断,假如属性以@
或 v-on
开头,就是要进行事件绑定了。在 ast 对象中加上了 events
,并将 click 加到里面。
另外,当我们将模板修改为@click.once=handleclick('abcd')
的时候,events
中生成的属性会变成~click
。
绑定属性中,once
stop
这些称为 modifier
。vue 针对事件监听
中的modifier
,做了特殊的处理,方便后续阶段进行相应的处理。
- capture -> !
- once -> ~
- passive -> &
render
// render 字符串
with(this){return _c('div',{on:{"click":function($event){return handleClick('abcd')}}},[_v("fjskdflds")])}
字符串会通过 new Function(code)
的方式创建一个函数。
从字符串中可以看出,我们的click 事件函数
是 on 对象中的一个属性click
。可以很自然的联想到,之后可能会用addEventListener('click', fn)
去添加相应的函数(其实不是)。
同时,在渲染函数中,我们的代码有了一些变化。
click
函数被包裹在一个带有$event
变量的函数中。这也就不难理解,为什么我们可以在自己的模板字符串(如 handleClick('abcd', $event)
)中使用$event
,从而得到原生的事件对象了。因为创建函数以后,这个变量在我们函数的作用域上层。
通过修改字符串模板,最后创建出来真实的函数,这种方式很神奇。
patch 阶段
render 函数生成 vnode。根据 vnode 进行 patch 的过程中,定义了一些钩子函数,如 create
update
。在 patch 的不同阶段进行调用,事件就是通过这些钩子函数绑定上去的。
这些钩子函数在/platforms/web/runtime/modules
文件夹中,现在我们只关心 events.js
。
可以发现,在create
update
时,实际上都是将vnode
传给updateDOMListener
,这个函数负责了 DOM 事件的创建和更新。该函数实际上是/src/core/vdom/helpers/update-listeners.js
。
update-listener
export function updateListeners (
on: Object,
oldOn: Object,
add: Function,
remove: Function,
createOnceHandler: Function,
vm: Component
) {}
函数遍历 on 对象,通过normalizeEvent
函数处理特殊的属性名,将其转为参数,也就是once
passive
等。
然后根据新旧 vnode 对比,更新、替换、删除事件函数。
// 最终的 vnode
{
tag: 'div',
data: {
on: {
click: function invoker() {}
}
},
}
实际上,我们事件函数会再被封装一次,包裹在一个名为 invoker 的函数中
- 该函数由
createFnInvoker
创建,将我们的函数包裹在一个异常处理代码块中执行。 - 我们的函数实际上实际上是
invoker
函数的一个属性fns
,当事件触发时,调用的是 invoker,invoker 再找我们的函数。这样的话,当我们的事件函数变化时,只需要修改这个属性,不需要removeEventListener
parentListeners
再回到之前初始化的例子,做一点修改
Vue.component('child', {
template: '<div>child</div>'
})
let vue = new Vue({
el: '#app',
template: `
<child @click="handleClick('abcd')"></child>
`,
methods: {
handleClick (a) {
console.log(a)
}
}
})
这个时候,我们的 click 事件是绑定在子组件上的。这就和真实 dom 元素的事件有区别了。
我们知道 vue 实例可以通过 $emit
触发事件,$on
绑定事件,父子组件之间可以进行通信,不需要使用浏览器的 API。
_parentListeners 就是父组件需要在子组件注册的函数。过程同样是调用updateListeners
,区别就是后面的参数,add
remove
函数。之前的例子中,add 函数是 addEventListener
,在这个例子中是 $on
。
其他问题
vnode是虚拟节点,什么时候将这个函数挂载到真实DOM节点中?
patch的时候会根据vnode创建真实DOM节点,并且将其赋值为elm到vnode中,通过这个引用,添加函数
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
const on = vnode.data.on || {}
const oldOn = oldVnode.data.on || {}
target = vnode.elm
normalizeEvents(on)
updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
target = undefined
}
函数调用的过程中,怎么保证this指向当前vue实例?
在init阶段,initMethods过程中,如果判断属性是函数,会将其bind到当前实例。
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
vue component 初始化
VNode
一段简单 template
代码,如 <div a=1><child/></div>
,在经过 vue compiler 处理后,生成的 render 函数是这样的:
function render() {
with(this) {
return _c('div', {
attrs: {
"a": "1"
}
}, [_c('child')], 1)
}
}
这里的 _c
是 core/vom/create-element
中的 createElement
,从函数声明中也可以知道,该函数的作用是返回 VNode
,在后续的 patch
过程中使用。其他在 render
中使用的工具函数位于 core/instance/render-helpers
。
那 vue 是如何区分普通 DOM 节点和组件的呢?
怎么知道 div 是普通的节点,而 Child 是组件。
// core/vdom/create-element.js
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (typeof tag === 'string') {
let Ctor
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
// core/vdom/create-component.js
vnode = createComponent(Ctor, data, context, children, tag)
}
}
}
判断 tag
字符串是不是保留元素(HTML 和 SVG 中的 tag),来判断是什么类型。
普通节点
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
Vue 组件
vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children }, // componentOptions 选项
asyncFactory
)
可以从 VNode 的创建中看出一些不同点:
- 组件在 vnode 中没有 children 选项
- 组件多传了
componentOptions
参数- 包含
vue 构造函数
props
监听器
tag
children 子节点
- 包含
实现细节
// src/vdom/create-component.js
function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
data = data || {}
// 利用 options.props 选项,从 data 中提取 propsData
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// 从 data.on 中提取父节点传递的 listeners,事件监听器
// 对于组件来说,@click 并不是原生事件,而是通过 $on $emit API 模拟的事件
const listeners = data.on
// 组件中,native 事件会在组件根元素上监听一个原生事件
data.on = data.nativeOn
// data.hook = {}
// 在 hook 中增加 componentVNodeHooks
// 增加 init,prepatch,insert,destroy 钩子,在不同的时机中会被调用
installComponentHooks(data)
}
patch
if (isUndef(oldVnode)) {
createElm(vnode, insertedVnodeQueue)
} else {
if (sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}
}
简单来说 patch
有两种情况
- 旧节点不存在,就使用 VNode 创建真实 DOM 元素
- 同一个 VNode ,调用
patchVNode
进行更新
创建
createElm
中调用 core/vdom/patch.js
中的 createComponent
创建元素
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 1. 调用 vnode 过程中安装的 componentHooks
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// 2. 初始化组件,插入到 DOM 中
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
init 钩子
createComponentInstanceForVnode
为 vnode 创建componentInstance
- vnode 中两个与 vue 实例相关的属性为
componentOptions
和componentInstance
componentOptions
在创建 vnode 时生成componentInstance
在patch
过程中生成,新建一个vue 实例
- vnode 中两个与 vue 实例相关的属性为
- 空挂载
创建组件实例
组件本质还是一个 Vue 实例,但是与 root
实例初始化过程中存在一些差异,所以在流程上也有一些区别。比如,根实例不需要从父组件获取数据,而组件需要处理 propsData
。
function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
): Component {
debugger
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
// 使用 Ctor 构造函数创建一个 vue 实例
// 组件原本的 options 存到 Ctor.constructor 中
// 等同于 new Vue()
// 由于是组件,通过 _isComponent 选项,走不同的 init 过程
return new vnode.componentOptions.Ctor(options)
}
构造函数
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
vm._isVue = true
if (options && options._isComponent) {
initInternalComponent(vm, options)
}
}
function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
如果是组件,会到 vnode 的 componentOptions
中得到一些数据,然后继续走正常的初始化流程。
vue render 和 slot 的使用
slot
作用: 通过slot可以向子组件插入内容。
像子组件传递一些父组件创建的dom内容
具名插槽
可以通过具名插槽,分配多个插槽位置,但是必须存在一个默认插槽
。
也就是说,使用具名插槽时,意味着有多个名字的slot
,这个时候必须有个名为deafult
的slot
,或者<slot></slot>
Vue.component('test', {
template: `
<span>
<header>
<slot name="default"></slot>
</header>
<footer>
<slot name="footer"></slot>
</footer>
</span>
`
})
new Vue({
el: '#app',
template: `
<test>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</test>
`
})
作用域插槽
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
父组件通过slot
向子组件传递DOM,通过作用域插槽
,可以让父组件访问子组件的数据
Vue.component('current-user', {
template: `
<span>
{{ user }}
<slot v-bind:user="user"></slot> // 子组件传递数据
</span>
`,
data () {
return {
user: 'jiangkunhe'
}
}
})
new Vue({
el: '#app',
template: `
<current-user>
<template v-slot="slotProps"> //父组件使用子组件的数据
{{ slotProps }}
</template>
</current-user>
`
})
render
一个vnode对象使用的属性深入数据对象
render函数产生vnode,其中一个重要的函数是createElement
createElement
传递tag
data 也就是数据对象
children
生成 vnode
new Vue({
el: '#app',
render (createElement) {
return createElement('div', {
on: {
click: this.handleClick
}
}, [
'helloworld'
])
},
methods: {
handleClick () {
console.log('handleClick run')
}
}
})
单文件组件中的写法
可以像react
一样,类似写脚本的形式使用
<script>
export default {
render (h) {
return (
<span>
{ this.a }
</span>
)
},
data() {
return {
a: 'fsjdkfjsdkl'
}
},
}
</script>
vuex
在 vue 项目中,父子组件通信是比较容易的,但是我们有时候也需要跨代之间的通信,兄弟组件的通信,有些数据是需要共享的。
就像文档中所说:
Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。
从源码看,创建一个 Vuex.store
主要做了四件事
class Store {
constructor (options = {}) {
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
this._modules = new ModuleCollection(options)
installModule(this, state, [], this._modules.root)
resetStoreVM(this, state)
}
}
- 安装
- 注册 module
- 安装 module
- 用 store 创建一个 vue
安装
首先我们知道安装 vuex 有两步:
- Vue.install(Vuex)
- new Vue({ store })
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
Vue.mixin({ beforeCreate: vuexInit })
function vuexInit () {
const options = this.$options
// store injection
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
}
vuex 安装过程中通过 mixin,为所有 vue 实例增加了 breforeCreate
函数,即 vuexinit
。
实例化 Vue 应用的时候,传递定义的 store
,这意味着在根 Vue 实例中可以通过 $options
拿到 store
。
所以分成两种情况
- 根 Vue 对 store 进行实例化。
- 子组件通过 parent 拿到 store。
这样所有的组件都得到 $store
,注入就完成了。
注册 module
const store = new Vuex.Store({ ...options })
我们创建 store
是通过传 options
的方式,因此需要对其进行处理,生成相应的数据结构。
我们传递的 options 是有 module 的层级关系的,一个 module 可以拥有子模块。
const store = new Vuex.Store({
// 拥有子模块 a 和 b
modules: {
a: moduleA,
b: moduleB
}
})
ModuleCollection
做的事情就是将 options 转换成实际的 Module
对象,并且维护他们的父子关系。实际上就是使用对象生成模块树。
安装模块
安装模块就是将 module 中的 state
getter
action
mutation
放到 store
中,后面我们就可以通过 store
调用定义好的接口。
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
// register in namespace map
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
// set state
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
}
const local = module.context = makeLocalContext(store, namespace, path)
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
代码非常整洁,做了四件事:
- 计算 namespce
- 将模块的 state 放到
store.state
中 - 遍历,将
mutation
action
getter
放到 store 中 - 递归注册子模块
命名空间 (namespce)
代码开头就获取了当前模块的 namespce,后续注册 mutation
等都和这个东西有关,可以说是非常重要的。
ModuleCollection 根据当前模块的路径(模块的 key 值维护的数组)以及是否设置了 namespced 计算出 namespce
const store = new Vuex.Store({
modules: {
a: moduleA, // path = ['a']
b: {
modules: {
namespced: true
c: moduleC // path = ['b', 'c']
}
}
}
})
getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
// 有命名空间的情况, 比如命名为 a, 最后的 namespace 为 a/
// 最后有斜杠,因为还要追加 mutations actions 的 key 值
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
在这个例子中,a、b 模块的 namespce 都为 ''
, c 模块的为 c/
然后使用 mutation
action
getter
的 key 拼接成一个新的 key 值作为最终的 namespcedType
存储起来。
这样会导致,a、b 模块中相同 key 值的 mutation
action
会被一个 commit
或 dispatch
触发。
默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。
我觉得这是一个坑点,大部分时候我们都会保持唯一的命名。所以设置模块的时候,应该尽量设置 namespced = true
用 store 创建一个 vue
function resetStoreVM (store, state, hot) {
// bind store public getters
store.getters = {}
// reset local getters cache
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// direct inline function use will lead to closure preserving oldVm.
// using partial to return function with only arguments preserved in closure environment.
computed[key] = partial(fn, store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
// use a Vue instance to store the state tree
// suppress warnings just in case the user has added
// some funky global mixins
const silent = Vue.config.silent
Vue.config.silent = true
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store)
}
}
在 vuex 中,我们的数据分为两种形式:
- state,通过 store.state 获取
- getter,通过 store.getter 获取
由于我们传递的模块 getter 是函数,提供的 API 是直接取值,因此需要根据 key 值定义 get 函数,同时将函数存到 computed
对象中,这样做使 getter 被使用的时候才会计算,是一个优化。
最后用 state
computed
创建一个 vue。
为什么需要创建 vue
将 getter 函数转换为 vue 中的计算属性的好处已经说过了。同时很重要的一点是,store
中的属性得是响应式属性
。为什么呢?
我们可以通过 this.$store
可以获取到数据,数据可能要在模板中使用,比如 <template><div>{{ $store.state.count }}</div></template>
。因此必须要转换成响应式属性,才能触发模板更新。
其他问题
怎么保证只有 mutation 才能修改 state ?
function enableStrictMode (store) {
store._vm.$watch(function () { return this._data.$$state }, () => {
if (process.env.NODE_ENV !== 'production') {
assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
}
}, { deep: true, sync: true })
}
function _withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
commit 函数会标记一个 flag,同时使用 watcher 监听 state 属性。如果发现,状态改变了,并且没有这个标记,那么就不是正常的修改,在非生产环境给出提示。
我们知道,vue 中 watcher 的执行时一般异步的,但如果需要拦截到这个状态,比如在fn
执行结束之前触发 watcher。因此设置了 sync: true
,这个选项意味着,一旦数据进行了set
操作,watcher 会马上执行。
这个选项在 vue 文档中没有提到,毕竟 vue 使用 queue 的方式进行了优化,一般我们写代码也没有这么强烈的同步需求,可能会被滥用。
一些想法
vuex 的代码写的很精巧,设计的很好。自己平时写代码很少写这样的类设计。
原本以为 vuex 是通过 provide/reject
实现的,后面查文档时发现
提示:
provide
和inject
绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
vue 和它的响应式原理是密不可分的。只要对象是响应式的,就可以触发它的机制,那么也就不需要使用provide/inject
这种方式。像 vuex 一样单独抽离出一个 vue 对象就可以了,这和 eventBus
有些类似。
vue-router
最近在学习vue-router
,感觉比想象中复杂。
整体流程
根据流程大致画了一个思维导图,也可以参考滴滴前端博客中的流程图。
vue-router
的安装过程
- 使用
Vue.use
,调用插件的install
函数- 在
Vue.prototype
中挂载方法,暴露接口,$router
$route
Vue.component
提供公共组件,router-view
router-link
- 利用
mixin
混入生命周期和属性,beforeCreate
_routerRoot
_router
- 在
new Vue()
的过程中传入选项
beforeCreate () {
if (isDef(this.$options.router)) {
// options 中传递了 router 选项,设置 _routerRoot 为自己
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 取父组件的 _routerRoot
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
}
跟vuex
非常类似,在beforeCreate
中判断当前组件的options
中有没有定义 router
(也就是VueRouter
实例),没有的话就取父组件的。
通过传递的方式,在所有组件**享了VueRouter
。
视图更新
视图更新依赖router-view
组件。
vue-router
提供了router-view
组件用于展示视图,组件的render
函数中使用了一个响应式数据_route
,因此当_route
变化的时候,会通知router-view
组件进行更新,渲染相应的组件。
// _route 是一个 Route 对象
declare type Route = {
path: string;
name: ?string;
hash: string;
query: Dictionary<string>;
params: Dictionary<string>;
fullPath: string;
matched: Array<RouteRecord>; // 根据当前路由路径匹配的 RouteRecord 数组
redirectedFrom?: string;
meta?: any;
}
declare type RouteRecord = {
path: string;
regex: RouteRegExp; // path-to-regexp 生成的正则
components: Dictionary<any>; // 对应我们编写 VueRouter 中传的组件
instances: Dictionary<any>;
name: ?string;
parent: ?RouteRecord;
redirect: ?RedirectOption;
matchAs: ?string;
beforeEnter: ?NavigationGuard;
meta: any;
props: boolean | Object | Function | Dictionary<boolean | Object | Function>;
}
_route
对象更新的途径有两种:
- 通过调用
vue-router
提供的接口,如push
go
replace
- 浏览器事件,
popstate
和hashchange
。取决于vue-router
的模式,监听不同的事件。
实际上,这两种方式最终都会调用transitionTo
函数,该函数在路由成功跳转后,会通过history.listen
的回调形式更新_route
,从而更新视图。
浏览器事件
在 Web 环境中,我们可以使用 hash
history
两种路由模式,默认情况下会使用 hash
模式。
hash
当 URL 中的 hash 值改变时,就会触发 hashchange
事件,并且会留下记录。
history
通过 HTML5 提供的 History API 进行模拟,访问和操作历史记录,不会刷新页面,提供了主动改变浏览记录的能力。
当用户点击前进后退,或者我们调用 API 前进后退时,会触发 popstate
事件。IE 10 以上才支持。
阅读源码后发现,hash
模式并不一定监听hashchange
事件。
export const supportsPushState =
inBrowser &&
(function () {
const ua = window.navigator.userAgent
if (
(ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
ua.indexOf('Mobile Safari') !== -1 &&
ua.indexOf('Chrome') === -1 &&
ua.indexOf('Windows Phone') === -1
) {
return false
}
return window.history && typeof window.history.pushState === 'function'
})()
// 如果当前环境支持 Histroy API 就会使用 History API 进行模拟
const eventType = supportsPushState ? 'popstate' : 'hashchange'
window.addEventListener(
eventType,
handleRoutingEvent
)
两个事件的一些差异
当使用History API
调用 pushState
replaceState
时,不会触发popstate
事件。而页面路由发生变化,一定会触发hashchange
。
在使用hash
路由时,使用router API
改变路由后,还会触发一次hashchange
。
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
// 相同的路由不会继续执行逻辑
if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
route.matched.length === current.matched.length
) {
this.ensureURL()
return abort(createNavigationDuplicatedError(current, route))
}
在history
的基类中,confirmTransition
函数保证相同路由不会继续执行,因为路由跳转的过程中,会触发相应的钩子函数,以及执行路由守卫。
其他 API
scrollBehavior
有时候在一些详情页,只是替换了路由中参数 ID,复用了组件,然而滚动条会保持原来的位置。之前没有发现vue-router
提供了滚动行为的接口,导致自己去监听$route
变化重置滚动条。
命名视图
declare type RouteRecord = {
path: string;
regex: RouteRegExp;
components: Dictionary<any>; // 对应我们编写 VueRouter 中传的组件
}
可以从RouteRecord
的实现中看到,components
是一个字典,也就是对象,这意味着在一个router-view
中可以表示多个组件。
// 或许可以通过绑定属性,动态改变 name 显示不同的组件,还没有想到有什么实践方式
<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
<router-view class="view three" name="b"></router-view>
参考
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.