shen-zhao / blog Goto Github PK
View Code? Open in Web Editor NEW笔记
笔记
// instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
function instanceofFn(left, right) {
var target = right.prototype
while (left) {
left = left.__proto__
if (target === left) {
return true
}
}
return false
}
写得比较匆忙,history、路由调度没有完成,未完待续...
路由安装利用了Vue.minxin全局注册了生命周期钩子,以及处理一些运行时的逻辑
// router注册
const router = createRouter({/* options */})
const vm = Vue({
// ...
router
})
// install.js
Vue.mixin({
beforeCreate () {
// 组件选项只有包含router,才会执行init,并且注册响应式的currentRoute信息
if (isDef(this.$options.router)) {
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
}
// 这个方法通过routerView用来连接matcher和组件实例,后续会通过组件实例调用生命周期钩子函数
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
小技巧:有时我们会有自定义弹窗的逻辑,使用Vue.extend生成并手动插入dom,如果弹窗中如果需要使用router或store的时候,也需要像根组件一样对router、store进行注册。
路由安装阶段,注册了很多运行时的逻辑,其中包括了
保存组件实例是为了更新时调用组件的声明周期钩子
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
// 这个操作是在确认一件事情
// 就是当前组件的父组件是否为router-view组件,如果是的话,调用这个router-view中的registerRouteInstance方法
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
递归处理返回三个数据:
type PathList = Array<string>
type pathMap = {
[path: string]: RouteRecord
}
type nameMap = {
[name: string]: RouteRecord
}
interface RouteRecord = {
path: string; // 路由路径
regex: RouteRegExp; // 路由匹配正则
components: object; // 路由组件map
instances: object; // 路由组件实例map
name?: string; // 路由名称
parent?: RouteRecord; // 父级路由
redirect?: RedirectOption;
matchAs?: string;
beforeEnter: NavigationGuard; // 路由守卫
meta: any;
props: boolean | Object | Function | Array<boolean | Object | Function>;
}
match 函数是一个闭包,处于 createMatcher 作用域内,所以可以使用第一步处理routes产生的三个数据结构
match主要逻辑分为两个分支:
interface Route = {
path: string;
name: ?string;
hash: string;
query: object;
params: object;
fullPath: string;
matched: Array<RouteRecord>;
redirectedFrom?: string;
meta?: any;
}
这个结构就是真实的路由结果,除了包括路由的初始参数和元信息外,还有用户参数数据、全路径、匹配结果等信息
在创建route的过程中,最重要的一个环节就是获取record匹配结果:
路由创建方法,逻辑比较简单
function createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: ?Location,
router?: VueRouter
): Route {
const stringifyQuery = router && router.options.stringifyQuery
let query: any = location.query || {}
try {
query = clone(query)
} catch (e) {}
const route: Route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
matched: record ? formatMatch(record) : [] // 就是这里
}
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
}
return Object.freeze(route)
}
formatMatch来获取匹配结果,我们来见识一下
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
const res = []
while (record) {
res.unshift(record)
record = record.parent
}
return res
}
原来这里逻辑这么简单,关键点在于创建路由record的时候,子路由保存了父级路由的引用,通过循环我们一步一步找到这个路由的所有上层路由的record,对于返回结果的顺序,是父辈在左子辈在右,这个顺序对于匹配组件在router-view的渲染至关重要,因为我们知道,vue-router的渲染规则是根据router-view的嵌套深度决定的,这个嵌套深度与匹配结果是一致的。
这里我们直接跳到router-view组件里面一探究竟,来印证一下:
router-view component render
// determine current view depth, also check to see if the tree
// has been toggled inactive but kept-alive.
let depth = 0
let inactive = false
while (parent && parent._routerRoot !== parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++
}
if (parent._inactive) {
inactive = true
}
parent = parent.$parent
}
data.routerViewDepth = depth
// render previous view if the tree is inactive and kept-alive
if (inactive) {
return h(cache[name], data, children)
}
const matched = route.matched[depth]
上面的代码就是router-view组件的实现逻辑,我们发现组件渲染时,通过对父辈组件的遍历来识别是否有父辈router-view组件,一直遍历到routerRoot,得到的深度depth,就是当前router-view组件的深度,最后通过depth为下标取出匹配结果种的目标结果,到这里,真实需要渲染的组件也就得到了。
history有三种模式:
注意:调用pushstate不会触发popstate事件
history驱动路由的匹配和渲染:
最近发现了一个比较大的认知错误,一直以为vue官方说不支持数组的索引进行get和set就是因为Object.defineProperty
不支持数组索引的拦截,这个认知真是打错特错了,自动动手测试了一下,数组索引是可以拦截的,静下心来思考一下,本来在js中数组就是一种特殊的对象,数组的索引其实就是对象的属性,理应和对象的行为是一样的,通过这个问题我们得到一个教训:一定要自己实践去验证,要不然误人子弟!!!
既然如此,那么vue 2为什么没有对数组的索引定义响应式属性呢?这个问题在这个文章中有一定结论,总的来说:性能代价和实际用户体验的权衡,未来我也会专门写一篇文章来探讨一下。所以下面探讨的内容的动机也就变成了:vue 2中对于数组索引没有使用get/setter,那么如何实现自动响应呢?
可能大家都比较了解vue 2基于 Object.defineProperty
实现的响应式原理,以及通过 getter/setter
实现自动依赖收集的过程,不过 Object.defineProperty
这个api有一些缺陷:
虽然 Object.defineProperty
有上述的缺陷,但是vue 2还是实现了几乎全场景的自动响应和依赖收集(只限对象和数组,数组不能索引操作)
$set
可以实现对于某个对象新增响应式属性,并触发一次 notify
,而且对于数组也是有效的,那么它到底是如何确保watcher
能够收集数组的依赖而进行更新呢?
让我们在回顾一下响应式对象的定义和依赖收集过程
class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 对对象设置响应式标识
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 复制代理方法
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
响应式对象定义的实现是通过上面的 Observer
类
属性:
方法:
实例化过程中,判断 value
类型,如果是对象,则对对象的的属性定义访问器属性,如果是数组,则进行其他特殊处理。
了解vue 2响应式的同学可能都知道,在定义对象的访问器属性时,对于每个属性都会定义一个 Dep
实例,并存在闭包当中,用于后续的属性触发依赖收集和通知更新(notify
),但是定义响应式对象的构造器为什么也会存在一个 Dep
实例呢?这个是我一直没有搞明白的地方。
同时疑问也来源于 getter
里的这段代码:
// defineReactive
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
/* 疑问开始 */
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
/* 疑问结束 */
}
return value
},
正如上面这段代码中注释的位置,我一直搞不明白这里是为了做什么。
疑问:watcher对于该属性的依赖已经收集了,为什么还要对属性值为对象的进行依赖收集呢?
这个疑问存在了好久,当某次再次重温这段代码的时候,突然想到,既然这里可以收集依赖,那么将来必定会有通知(notify
)依赖更新的逻辑,所以全局搜索的了一下 notify
方法名,出现的位置(排除setter
里的):
$set
原始实现$del
原始实现我们先来研究一下 $set
方法
暂时跳过数组的相关逻辑
function set (target: Array<any> | Object, key: any, val: any): any {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) { // 判断vmCount来判断是否是root $data
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
// 这里进行了通知
ob.dep.notify()
return val
}
这个方法的主要逻辑是对一个对象的属性进行扩展,如果对象中已经存在该属性,则直接设置新值后返回,如果没有这个这个属性,首先判断这个对象是不是一个响应式对象(__ob__
),如果不是,直接设置属性和值后返回,如果是响应式对象,则对这个新属性设置访问器属性。然后通知这个依赖于该对象的 watcher
进行更新(ob.dep.notify
)
在 $set
方法里我们发现了 ob.dep.notify
的调用,这个 dep
就是在 Observer
实例中针对响应式对象的 Dep
实例,这既然有 notify
,那就意味着 dep
会在某个时机触发依赖收集,那么 Observer
实例中的 dep
在哪个环节进行的依赖收集呢?还记得刚才提出的疑问代码吗,不错,就是上面 getter
里的逻辑,让我们再来看一下:
// defineReactive
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
/* Observer dep 收集开始 */
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
/* Observer dep 收集结束 */
}
return value
},
正是这段逻辑触发了依赖收集,具体分析下:childOb
是对 value
进行响应式处理(构造 Observer
),如果 childOb
存在,说明 value
是一个对象或数组,当属性读操作正好读取到这个 value
的时候,watcher
除了收集属性本身的依赖,顺便也针对这个 value
对象进行收集,只有这样,在运行时针对这个 value
进行 $set
操作时,才能正确的通知 watcher
更新。
局限性的:只有父级进行属性访问的时候,子级
value
才能触发value
本身的依赖收集,所以进行$set
是,target
参数不能设置$data
(组件根data
)本身,只能针对$data
的属性值。
本身对于 data
选项还有一个局限性:data
本身必须返回的是纯对象([Object object]
),不能是一个数组,因为数组无法对下标设置访问器属性,这个在 data
初始化时已经进行了处理:
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// data必须是一个纯Object
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// .....
}
同理,$del
与 $set
原理类似:
function del (target: Array<any> | Object, key: any) {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
if (!hasOwn(target, key)) {
return
}
delete target[key]
if (!ob) {
return
}
ob.dep.notify()
}
开篇我们提到了 ,那么vue2是如何实现数组修改的自动响应呢?Object.defineProperty
不支持数组
vue 2通过对数组的一些原始方法的代理,实现了数组的响应,但是对于数组的索引操作还是无能为力,所以vue2建议操作数组使用数组的原生方法进行操作。
下面我们来了解一下vue 2如何对数组的方法进行代理,并如何进行依赖收集的
回到 Observer
类(只保留了数组的处理):
class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
实例化 Observer
类时,会判断 value
是否为数组,当为数组时对数组进行了如下处理:
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
不管是 protoAugment
还是 copyAugment
都是为了改写需要代理的数组方法:
我们看一下如何进行方法代理:
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted) // !!!
// notify change
ob.dep.notify() // !!!
return result
})
})
这里逻辑比较简单,就是常规对于方法的重写代理,关键点在于对于进行依赖通知以及新添加元素的响应式处理。
observeArray
方法,对数组的每一项进行响应式处理,这样做也是为了手动跳过数组索引不能拦截的问题,直接访问数组每一项并尝试进行响应式注册:
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
notify
逻辑比较容易理解,那么数组时如何进行依赖收集的呢?其实在探究 $set
时已经出现答案了,当属性访问器触发 getter
时,会进行该属性的依赖收集,同时如果属性值为对象或数组,会同时触发属性值(Observer.dep
)本身的依赖收集,数组更为特殊,不仅触发自己的依赖收集,还会对数组的每一项触发收集。
为什么需要对数组的每一项执行收集呢?因为数组不像对象那样属性被代理后同时收集 value
对象的代理,因为数组每一项的访问拦截是断层的,arr[i]不会触发get,如果此项是一个对象,此对象不会触发依赖收集,由于数组本身依赖收集就需要对象属性访问的支持,所以在数组进行收集时,必须也对数组的每一项(对象)进行依赖收集,这样做就绕过数组每一项的访问无法被拦截的问题而直接访问每一项并收集当前依赖,如果不这样做,对于数组项中的对象进行 $set
操作就会失效
,因为该对象没有被依赖,就无通知可发,但是$set新增的属性有可能真的在这个组件中使用,这就会导致组件没有更新,这样的错误是不应该出现的!。
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
为了实现数组的响应式处理,在 Observer
类中也存在一个 Dep
实例进行依赖收集,这样可以通过一定约束(根data
必须为纯对象),在对属性进行依赖收集时同时对属性值为对象或数组类型的进行 Observer
构造,使 value
也能收集依赖,根本上来说这是为了解决数组响应式才采取的方案,通过这个方案,同时可以实现对对象和数组的运行时进行 $set 和 $del 的能力,不得不惊叹尤大的巧妙设计!
// 最简版
function bind(fn, context) {
return function () {
return fn.apply(context, Array.prototype.slice.call(arguments))
}
}
// 预置参数
function bind(fn, context) {
var args = Array.prototype.slice.call(arguments, 2)
return function () {
return fn.apply(context, args.concat(Array.prototype.slice.call(arguments)))
}
}
// new操作符,忽略thisArg
function bind(fn, context) {
var argsPre = Array.prototype.slice.call(arguments, 2)
function bindFn() {
context = this instanceof bindFn ? this : context
const args = argsPre.concat(Array.prototype.slice.call(arguments))
return fn.apply(context || null, args)
}
bindFn.prototype = fn.prototype
return bindFn
}
// new优化版,不修改原函数的原型对象
function bind(fn, context) {
var argsPre = Array.prototype.slice.call(arguments, 2)
function Fake(){}
function bindFn() {
context = this instanceof bindFn ? this : context
const args = argsPre.concat(Array.prototype.slice.call(arguments))
return fn.apply(context || null, args)
}
Fake.prototype = fn.prototype
bindFn.prototype = new Fake
return bindFn
}
我们都知道 Vue 2
内部实现了事件的发布订阅,不仅在 Vue
内部机制中使用,开发人员经常把它当做事件总线来使用,主要 Api
如下:
但是在 Vue 3
中,只剩下了 $emit
,其余 Api
全部移除了,因为 Vue 3
内部不再需要这套事件发布订阅机制,所以没有必要实现,这样也能减小 Vue
的体积,倘若开发人员需要使用事件发布订阅模式,完全可以自己实现或者使用其他现成的类库。
那么vue 3脱离了事件发布订阅机制,怎么实现事件系统呢?
研究事件之前我们来看一下 Vue 3
中 props
的变化,Vue 3
中 VNode
现在是一个扁平的 prop
结构,包括用户自定义的属性和事件回调,这个改变使 prop
的结构变得简单,也有利于其他功能的实现,比如事件系统,对比一下 2.x
和 3.x
的 props
结构:
// 2.x 中的prop是属于嵌套结构
{
staticClass: 'button',
class: { 'is-outlined': isOutlined },
staticStyle: { color: '#34495E' },
style: { backgroundColor: buttonColor },
attrs: { id: 'submit' },
domProps: { innerHTML: '' },
on: { click: submitForm },
key: 'submit-button'
}
// 3.x 语法,扁平化
{
class: ['button', { 'is-outlined': isOutlined }],
style: [{ color: '#34495E' }, { backgroundColor: buttonColor }],
id: 'submit',
innerHTML: '', // dom属性
onClick: submitForm, // 事件回调
key: 'submit-button'
}
version: 3.0.0-beta.4
既然 $emit
还存在,那么先了解一下其具体实现:
// emit方法绑定组件实例
// packages/runtime-core/src/component.ts #484
instance.emit = emit.bind(null, instance)
// packages/runtime-core/src/componentEmits.ts #46
export function emit(
instance: ComponentInternalInstance,
event: string,
...rawArgs: any[]
) {
const props = instance.vnode.props || EMPTY_OBJ
// 开发环境,如果触发的事件没有在emit或props选项中声明时,警告开发者
if (__DEV__) {
const {
emitsOptions,
propsOptions: [propsOptions]
} = instance
if (emitsOptions) {
if (!(event in emitsOptions)) {
if (!propsOptions || !(toHandlerKey(event) in propsOptions)) {
warn(
`Component emitted event "${event}" but it is neither declared in ` +
`the emits option nor as an "${toHandlerKey(event)}" prop.`
)
}
} else {
const validator = emitsOptions[event]
if (isFunction(validator)) {
const isValid = validator(...rawArgs)
if (!isValid) {
warn(
`Invalid event arguments: event validation failed for event "${event}".`
)
}
}
}
}
}
let args = rawArgs
// 判断是否为双向绑定事件
const isModelListener = event.startsWith('update:')
// for v-model update:xxx events, apply modifiers on args
const modelArg = isModelListener && event.slice(7)
if (modelArg && modelArg in props) {
// 修饰符名称
const modifiersKey = `${
modelArg === 'modelValue' ? 'model' : modelArg
}Modifiers`
const { number, trim } = props[modifiersKey] || EMPTY_OBJ
if (trim) {
args = rawArgs.map(a => a.trim())
} else if (number) {
args = rawArgs.map(toNumber)
}
}
// convert handler name to camelCase. See issue #2249
// 转换事件名为 `onXxx`
let handlerName = toHandlerKey(camelize(event))
// 匹配事件回调
let handler = props[handlerName]
// for v-model update:xxx events, also trigger kebab-case equivalent
// for props passed via kebab-case
if (!handler && isModelListener) {
handlerName = toHandlerKey(hyphenate(event))
handler = props[handlerName]
}
// 执行事件回调
if (handler) {
callWithAsyncErrorHandling(
handler,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args
)
}
// 执行一次性回调
const onceHandler = props[handlerName + `Once`]
if (onceHandler) {
if (!instance.emitted) {
;(instance.emitted = {} as Record<string, boolean>)[handlerName] = true
} else if (instance.emitted[handlerName]) {
return
}
callWithAsyncErrorHandling(
onceHandler,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args
)
}
}
如果需要触发一个用户的事件,只需要在组件内调用 emit
,并传入事件名,根据事件名解析 prop
中的回调并执行,这样就达到了事件绑定、事件触发的效果了,确实不需要一个完整的发布订阅的实现。
其中,VNode
中事件回调的 key
必须是已 on
开头的驼峰式命名,内部会把这种命名的prop
解析到事件对象中,当然如果使用 template
我们不用关心命名问题,跟 Vue 2
中的语法一样,在 template
编译后,事件 prop
会被编译成 onXxx
的形式。
注意:上述 emit
的过程是组件自定义事件的触发流程,Vue 3
也增加了 emits
选项给与特定的限制和功能的支持。
原生事件是由外部的输入设备交互所触发的,所以不需要关心事件的触发问题,关键是在于事件是在什么时候被注册的。
下面我们来追溯一下原生元素的创建以及事件的注册。
原生 dom
的 prop
格式也遵循上面的新规范,所以事件回调传递的方式是一样的,原生事件最终肯定是需要注册在 dom
上,那么我们就来分析一下 mountElement
中关于 props
的处理逻辑,废话少说,咱么直接定位到源码位置
// packages/runtime-core/src/renderer.ts #768
// props
if (props) {
for (const key in props) {
if (!isReservedProp(key)) {
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
if ((vnodeHook = props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
}
这里我们发现,对于 dom
的处理,Vue 采用依赖注入的方式,这样可以更好的提供跨平台能力,接下来我们来分析基于 web dom 的 patchProp
,代码定位:
// packages/runtime-dom/src/patchProp.ts #36
// 这里就是关于事件prop的处理
if (isOn(key)) {
// ignore v-model listeners
if (!isModelListener(key)) {
patchEvent(el, key, prevValue, nextValue, parentComponent)
}
}
// packages/runtime-dom/src/modules/events.ts #59
export function patchEvent(
el: Element & { _vei?: Record<string, Invoker | undefined> },
rawName: string,
prevValue: EventValue | null,
nextValue: EventValue | null,
instance: ComponentInternalInstance | null = null
) {
// vei = vue event invokers
const invokers = el._vei || (el._vei = {})
const existingInvoker = invokers[rawName]
if (nextValue && existingInvoker) {
// patch
existingInvoker.value = nextValue
} else {
// 解析参数option /(?:Once|Passive|Capture)$/
const [name, options] = parseName(rawName)
if (nextValue) {
// add
const invoker = (invokers[rawName] = createInvoker(nextValue, instance))
// 注册事件
addEventListener(el, name, invoker, options)
} else if (existingInvoker) {
// 移除事件
removeEventListener(el, name, existingInvoker, options)
invokers[rawName] = undefined
}
}
}
export function addEventListener(
el: Element,
event: string,
handler: EventListener,
options?: EventListenerOptions
) {
el.addEventListener(event, handler, options)
}
export function removeEventListener(
el: Element,
event: string,
handler: EventListener,
options?: EventListenerOptions
) {
el.removeEventListener(event, handler, options)
}
到了这里我们定位到了原生元素事件注册的地方,当原生元素创建后,紧接着会进行 props
处理,其中包括事件的注册。
我们都知道,在vue的template中事件是支持修饰符的,如下:
.stop
- 调用 event.stopPropagation()
。.prevent
- 调用 event.preventDefault()
。.capture
- 添加事件侦听器时使用 capture 模式。.self
- 只当事件是从侦听器绑定的元素本身触发时才触发回调。.{keyAlias}
- 仅当事件是从特定键触发时才触发回调。.once
- 只触发一次回调。.left
- 只当点击鼠标左键时触发。.right
- 只当点击鼠标右键时触发。.middle
- 只当点击鼠标中键时触发。.passive
- { passive: true }
模式添加侦听器其中**/(?:Once|Passive|Capture)$/**
这三个修饰符是属于原生监听的选项(第三个参数),所以在事件绑定(上面代码中)时已经处理,那么其他的修饰符在什么时候处理的呢、怎样处理的呢?
经过一顿操作,终于定位到了,当然,过程比较坎坷
修饰符这里也是基于不同平台而实现的,上层包为下层包提供具体 Api
实现,底层包实现函数名注入(编译层)
// packages/runtime-dom/src/directives/vOn.ts #7
// 修饰符守卫,
const modifierGuards: Record<
string,
(e: Event, modifiers: string[]) => void | boolean
> = {
stop: e => e.stopPropagation(),
prevent: e => e.preventDefault(),
self: e => e.target !== e.currentTarget,
ctrl: e => !(e as KeyedEvent).ctrlKey,
shift: e => !(e as KeyedEvent).shiftKey,
alt: e => !(e as KeyedEvent).altKey,
meta: e => !(e as KeyedEvent).metaKey,
left: e => 'button' in e && (e as MouseEvent).button !== 0,
middle: e => 'button' in e && (e as MouseEvent).button !== 1,
right: e => 'button' in e && (e as MouseEvent).button !== 2,
exact: (e, modifiers) =>
systemModifiers.some(m => (e as any)[`${m}Key`] && !modifiers.includes(m))
}
// # 28
/**
* 修饰符守卫执行包装层
* @private
*/
export const withModifiers = (fn: Function, modifiers: string[]) => {
return (event: Event, ...args: unknown[]) => {
for (let i = 0; i < modifiers.length; i++) {
const guard = modifierGuards[modifiers[i]]
if (guard && guard(event, modifiers)) return
}
return fn(event, ...args)
}
}
// packages/compiler-dom/src/transforms/vOn.ts #58
// template解析v-on修饰符
if (isNonKeyModifier(modifier)) {
nonKeyModifiers.push(modifier)
}
// #17
const isNonKeyModifier = /*#__PURE__*/ makeMap(
// event propagation management
`stop,prevent,self,` +
// system modifiers + exact
`ctrl,shift,alt,meta,exact,` +
// mouse
`middle`
)
// # 95
// 边一层注入修饰符包装函数
// context.helper(V_ON_WITH_MODIFIERS) 其实是包括函数名:'withModifiers'
if (nonKeyModifiers.length) {
handlerExp = createCallExpression(context.helper(V_ON_WITH_MODIFIERS), [
handlerExp,
JSON.stringify(nonKeyModifiers)
])
}
我们通过一个例子来看一下编译的结果:
<button @click.stop="handleClick">按钮</button>
编译结果:
import { withModifiers as _withModifiers, createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("button", {
onClick: _withModifiers(_ctx.handleClick, ["stop"])
}, "按钮", 8 /* PROPS */, ["onClick"]))
}
我们看到了_withModifiers
方法,这就是实际调用的地方,当有修饰符时,才会编译出这样的代码。
Vue 3
中删除了 Vue 2
中发布订阅机制的实现,改为更简单直接的实现方式,一方面减少了源码的体积,另一方面更加专注于框架本身,删除了一些没有必要的概念和实现方式,避免 Api
的滥用造成过高的维护成本,大道至简!
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.