记录学习过程
如果对其他人有所帮助,那就再好不过了😀
作用: 通过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>
`
})
一个vnode对象使用的属性深入数据对象
render函数产生vnode,其中一个重要的函数是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>
webpack 只能处理 JS 和 JSON 文件,其余的文件类型需要依靠 loader 处理。vue-loader 是怎么处理 vue 文件的呢?
参考 vue-loader
template
script
style
分为三个块 (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 流程中一些模块的职责
使用 vue-loader 时,需要在 webpack 配置中添加 VueLoaderPlugin
如 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'
如 import render from 'source.vue?vue&type=template'
vue-loader 返回文件中的某个具体 block
根据 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&'
通过文件内容以及路径哈希值生成 id
const id = hash(
isProduction
? (shortFilePath + '\n' + source)
: shortFilePath
)
在 query 中将 id 传递给 template 以及 style 块
将 scopeId 添加到 vue options 中
Vue({
render: ...,
_scopeId: 'data-v-xxxxxx'
})
vue 在 patch 过程中的 createElm
,调用 setScope(vnode)
,将其添加到 dom 节点上
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)
})
我们使用webpack
打包应用时,默认情况下一个入口最终产生一个JS文件。在使用vue-cli
进行打包时,除了app.js
之外,还有vendor.js
。
还有很常见的一种打包方式会将JS文件分为3个部分。
这样做有什么好处呢?
在生产环境中,一般会为静态资源加上文件指纹。假如业务代码和第三方库代码打包到一起,那么任何一点代码的改动都会让文件指纹发生改变。
第三方库代码一般是不会改变的,假如将它单独提取出来,打包成一个文件,在库代码没有发生变化的情况下文件指纹就不会修改。这样做可以更好的利用客户端的缓存能力,减少请求次数。
在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')
}
下面是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指纹是不会发生改变的。
在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 项目中,父子组件通信是比较容易的,但是我们有时候也需要跨代之间的通信,兄弟组件的通信,有些数据是需要共享的。
就像文档中所说:
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)
}
}
首先我们知道安装 vuex 有两步:
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
。
所以分成两种情况
这样所有的组件都得到 $store
,注入就完成了。
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)
})
}
代码非常整洁,做了四件事:
store.state
中mutation
action
getter
放到 store 中代码开头就获取了当前模块的 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
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 中,我们的数据分为两种形式:
由于我们传递的模块 getter 是函数,提供的 API 是直接取值,因此需要根据 key 值定义 get 函数,同时将函数存到 computed
对象中,这样做使 getter 被使用的时候才会计算,是一个优化。
最后用 state
computed
创建一个 vue。
将 getter 函数转换为 vue 中的计算属性的好处已经说过了。同时很重要的一点是,store
中的属性得是响应式属性
。为什么呢?
我们可以通过 this.$store
可以获取到数据,数据可能要在模板中使用,比如 <template><div>{{ $store.state.count }}</div></template>
。因此必须要转换成响应式属性,才能触发模板更新。
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.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
模式。
当 URL 中的 hash 值改变时,就会触发 hashchange
事件,并且会留下记录。
通过 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
函数保证相同路由不会继续执行,因为路由跳转的过程中,会触发相应的钩子函数,以及执行路由守卫。
有时候在一些详情页,只是替换了路由中参数 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>
一段简单 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 的创建中看出一些不同点:
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)
}
if (isUndef(oldVnode)) {
createElm(vnode, insertedVnodeQueue)
} else {
if (sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}
}
简单来说 patch
有两种情况
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
componentOptions
和 componentInstance
componentOptions
在创建 vnode 时生成componentInstance
在 patch
过程中生成,新建一个 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 demo。
let vue = new Vue({
el: '#app',
template: `
<div @click="handleClick('abcd')"></div>
`,
methods: {
handleClick (a) {
console.log(a)
}
}
})
从 Vue 的整个流程思考,看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 对象
{
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
,做了特殊的处理,方便后续阶段进行相应的处理。
// 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
,从而得到原生的事件对象了。因为创建函数以后,这个变量在我们函数的作用域上层。
通过修改字符串模板,最后创建出来真实的函数,这种方式很神奇。
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
。
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
再回到之前初始化的例子,做一点修改
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
。
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
}
在init阶段,initMethods过程中,如果判断属性是函数,会将其bind到当前实例。
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.