Code Monkey home page Code Monkey logo

blog's People

Contributors

shen-zhao avatar

Stargazers

 avatar  avatar

Watchers

 avatar

blog's Issues

instanceof模拟

instanceof MDN

// instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

function instanceofFn(left, right) {
  var target = right.prototype
  while (left) {
    left = left.__proto__
    if (target === left) {
    	return true
    }
  }
    
  return false
}

vue-router v2.8源码分析

写得比较匆忙,history、路由调度没有完成,未完待续...

路由安装(install)

路由安装利用了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进行注册。

路由安装阶段,注册了很多运行时的逻辑,其中包括了

  • 注册routerRoot
  • $route的get、set
  • 组件实例化时与router-view的绑定与连接(registerInstance)

保存组件实例是为了更新时调用组件的声明周期钩子

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)
    }
  }

路由初始化

  • 初始化参数选项
  • 初始化matcher(createMatcher)
  • 初始化History(history/hash/abstract)

1. 初始化matcher

a. 初始化matcher的第一步,先对用户的路由表(routers)进行处理

递归处理返回三个数据:

  • pathList 用来控制路由匹配的优先级
  • pathMap 以path为键的路由map
  • nameMap 以name为键的路由map
    分别对应下面三种结构:
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>;
}

b. 创建match函数(用于后续的实时匹配)

match 函数是一个闭包,处于 createMatcher 作用域内,所以可以使用第一步处理routes产生的三个数据结构
match主要逻辑分为两个分支:

  • 首先根据name匹配: 匹配时会组装动态params
  • 其次根据path匹配:根据pathList匹配,并解析动态params
    match函数返回值为route,由createRoute函数返回,究竟返回什么结构呢?
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

history有三种模式:

  • hash
  • history
  • abstract
    前两种常见与浏览器,提供的api:
  • push/replace
  • go
  • transitionTo(internal)
  • ...
    导航方式:
  • push/replace:命令式,执行匹配 => 渲染组件 => 修改url
  • 被动式:浏览器导航:前进后退、location.assign/replace,触发hashchange/popstate事件 => 执行匹配 => 渲染组件

注意:调用pushstate不会触发popstate事件

运行时

history驱动路由的匹配和渲染:

  • 页面初始导航直接调用transitionTo
  • redirect: 根据路由表配置,在match过成功修改route
  • 浏览器行为:后退、前进
  • 浏览器api:location.assign/replace
    匹配成功后更新当前激活的路由记录record,由于在router在安装时已经订阅了route的响应式属性,所以有使用route的组件(router-view或其他)在渲染过成功也被成功的订阅(依赖收集),所以当路由修改时,也会触发组件的重新渲染

Vue 2中array observer和$set如何实现

最近发现了一个比较大的认知错误,一直以为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

$set 可以实现对于某个对象新增响应式属性,并触发一次 notify,而且对于数组也是有效的,那么它到底是如何确保watcher能够收集数组的依赖而进行更新呢?

让我们在回顾一下响应式对象的定义和依赖收集过程

1. 响应式对象(数组)定义

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
  • dep
  • vmCount

方法:

  • constructor
  • walk
  • observeArray

实例化过程中,判断 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 原始实现
  • array特定方法的拦截实现

我们先来研究一下 $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()
}

Array Observer

开篇我们提到了 Object.defineProperty 不支持数组,那么vue2是如何实现数组修改的自动响应呢?
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 的能力,不得不惊叹尤大的巧妙设计!

bind的方法的模拟

bind API MDN

// 最简版
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 3事件系统

背景

我们都知道 Vue 2 内部实现了事件的发布订阅,不仅在 Vue 内部机制中使用,开发人员经常把它当做事件总线来使用,主要 Api 如下:

  • $on
  • $off
  • $once
  • $emit

但是在 Vue 3 中,只剩下了 $emit,其余 Api 全部移除了,因为 Vue 3 内部不再需要这套事件发布订阅机制,所以没有必要实现,这样也能减小 Vue 的体积,倘若开发人员需要使用事件发布订阅模式,完全可以自己实现或者使用其他现成的类库。
那么vue 3脱离了事件发布订阅机制,怎么实现事件系统呢?

VNode 扁平化

研究事件之前我们来看一下 Vue 3props 的变化,Vue 3VNode 现在是一个扁平的 prop 结构,包括用户自定义的属性和事件回调,这个改变使 prop 的结构变得简单,也有利于其他功能的实现,比如事件系统,对比一下 2.x3.xprops 结构:

// 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事件

原生事件是由外部的输入设备交互所触发的,所以不需要关心事件的触发问题,关键是在于事件是在什么时候被注册的。


下面我们来追溯一下原生元素的创建以及事件的注册。
原生 domprop 格式也遵循上面的新规范,所以事件回调传递的方式是一样的,原生事件最终肯定是需要注册在 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)
  ])
}

我们通过一个例子来看一下编译的结果:

https://vue-next-template-explorer.netlify.app/

<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 的滥用造成过高的维护成本,大道至简!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.