Vue3 源码解读系列(十二)——指令 directive

98 阅读4分钟

directive

定义:本质就是一个 JavaScript 对象,对象上挂着一些钩子函数。

实现:在元素的生命周期中注入代码。

指令注册

注册原理:把指令的定义保存到相应的地方,未来使用的时候可以从保存的地方拿到。

全局注册与局部注册的区别:

  • 全局注册存放在 instance.appContext
  • 局部注册存放在组件对象里
/**
 * 全局注册 app.directive
 */
function createApp(rootComponent, rootProps = null) {
  const context = createAppContext()

  const app = {
    _component: rootComponent,
    _props: rootProps,
    directive(name, directive) {
      // 检测指令名是否与内置的指令名有冲突
      if ((process.env.NODE_ENV !== 'production')) {
        validateDirectiveName(name)
      }

      // 没有第二个参数,则获取对应的指令对象
      if (!directive) {
        return context.directives[name]
      }

      // 重复注册的警告
      if ((process.env.NODE_ENV !== 'production') && context.directives[name]) {
        warn(/* ... */)
      }
      context.directives[name] = directive
      return app
    }
  }
  return app
}

指令解析

const DIRECTIVES = 'directives';
/**
 * 解析指令 - 根据指令名称找到保存的指令的对象
 */
function resolveDirective(name) {
  return resolveAsset(DIRECTIVES, name)
}

/**
 * 解析资源
 */
function resolveAsset(type, name, warnMissing = true) {
  // 获取当前渲染实例
  const instance = currentRenderingInstance || currentInstance
  if (instance) {
    const Component = instance.type
    // 先通过 resolve 函数解析局部注册的资源,如果没有则解析全局注册的资源
    const res = resolve(Component[type], name) || resolve(instance.appContext[type], name)
    // 如果没有,则在非生产环境下报警告
    if ((process.env.NODE_ENV !== 'production') && warnMissing && !res) {
      warn(/* ... */)
    }
    return res
  } else if ((process.env.NODE_ENV !== 'production')) {
    warn(/* ... */)
  }
}

/**
 * 解析
 */
function resolve(registry, name) {
  // 先根据 name 匹配,如果失败则把 name 转换成驼峰格式继续匹配,如果失败则把 name 变为驼峰式后再首字母大写继续匹配
  return (registry && (registry[name] || registry[camelize(name)] || registry[capitalize(camelize(name))]))
}

/**
 * 绑定指令 - 给 vnode 添加一个 dirs 属性,值为这个元素上所有指令构成的数组
 */
function withDirectives(vnode, directives) {
  const internalInstance = currentRenderingInstance
  if (internalInstance === null) {
    (process.env.NODE_ENV !== 'production') && warn(/* ... */)
    return vnode
  }
  const instance = internalInstance.proxy
  const bindings = vnode.dirs || (vnode.dirs = [])

  // 遍历 directives,拿到每一个指令对象以及指令的相关内容
  for (let i = 0; i < directives.length; i++) {
    let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
    if (isFunction(dir)) {
      dir = {
        mounted: dir,
        updated: dir
      }
    }
    bindings.push({
      dir, // 指令
      instance, // 组件实例
      value, // 指令值
      oldValue: void 0,
      arg, // 参数
      modifiers // 修饰符
    })
  }
  return vnode
}

指令生命周期

挂载

/**
 * 挂载元素
 */
const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  let el
  const { type, props, shapeFlag, dirs } = vnode

  // 创建 DOM 元素节点
  el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is)
  if (props) {
    // 处理 props,比如 class、style、event 等属性
  }
  // 处理子节点是纯文本的情况
  if (shapeFlag & 8/*TEXT_CHILDREN */) {
    hostSetElementText(el, vnode.children)
  }
  // 处理子节点是数组的情况,挂载子节点
  else if (shapeFlag & 16 /* ARRAY_CHILDREN*/) {
    mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || !!vnode.dynamicChildren)

    // 在元素插入到容器之前会执行指令的 beforeMount 钩子函数
    if (dirs) {
      invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
    }

    // 把创建的 DOM 元素节点挂载到 container 上
    hostInsert(el, container, anchor)

    // 在元素插入到容器之后会执行指令的 mounted 钩子函数  
    if (dirs) {
      // 通过 queuePostRenderEffect 进行包装,目的是组件 render 后同步执行 mounted 钩子
      queuePostRenderEffect(() => {
        invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
      })
    }
  }
}

/**
 * 执行指令 hook
 * @param {Object} vnode - 新 vnode
 * @param {Object} prevVNode - 旧 vnode
 * @param {Object} instance - 组件实例
 * @param {string} name - 钩子名称
 */
function invokeDirectiveHook(vnode, prevVNode, instance, name) {
  const bindings = vnode.dirs
  const oldBindings = prevVNode && prevVNode.dirs
  for (let i = 0; i < bindings.length; i++) {
    const binding = bindings[i]
    if (oldBindings) {
      binding.oldValue = oldBindings[i].value
    }
    const hook = binding.dir[name]
    if (hook) {
      callWithAsyncErrorHandling(hook, instance, 8/* DIRECTIVE_HOOK */, [vnode.el, binding, vnode, prevVNode])
    }
  }
}

更新

/**
 * 组件更新
 */
const patchElement = (nl, n2, parentComponent, parentSuspense, isSVG, optimized) => {
  const el = (n2.el = nl.el)
  const oldProps = (n1 && n1.props) || EMPTY_OBJ
  const newProps = n2.props || EMPTY_OBJ
  const { dirs } = n2

  // 更新 props
  patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG)
  const areChildrenSVG = isSVG && n2.type !== 'foreignObject'

  // 在更新子节点之前会执行指令的 beforeUpdate 钩子
  if (dirs) {
    invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
  }

  //更新子节点
  patchChildren(n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG)

  // 在更新子节点之后会执行指令的 updated 钩子
  if (dirs) {
    queuePostRenderEffect(() => {
      invokeDirectiveHook(vnode, null, parentComponent, 'updated')
    })
  }
}

卸载

/**
 * 组件卸载
 */
const unmount = (vnode, parentComponent, parentSuspense, doRemove = false) => {
  const { type, props, children, dynamicChildren, shapeFlag, patchFlag, dirs } = vnode
  let vnodeHook
  if ((vnodeHook = props && props.onVnodeBeforeUnmount)) {
    invokeVNodeHook(vnodeHook, parentComponent, vnode)
  }
  const shouldInvokeDirs = shapeFlag & 1/* ELEMENT */ && dirs
  if (shapeFlag & 6/* COMPONENT*/) {
    unmountComponent(vnode.component, parentSuspense, doRemove)
  } else {
    if (shapeFlag & 128/* SUSPENSE */) {
      vnode.suspense.unmount(parentSuspense, doRemove)
      return
    }

    // 在移除元素的子节点之前执行指令的 beforeUnmount 钩子
    if (shouldInvokeDirs) {
      invokeDirectiveHook(vnode, null, parentComponent, 'beforeUnmount')
    }

    // 卸载子节点,递归卸载自身节点和子节点
    if (dynamicChildren && (type !== Fragment || (patchFlag > 0 && patchFlag & 64/* STABLE_FRAGMENT */))) {
      unmountChildren(dynamicChildren, parentComponent, parentSuspense)
    } else if (shapeFlag & 16/* ARRAY_CHILDREN */) {
      unmountChildren(children, parentComponent, parentSuspense)
    }
    if (shapeFlag & 64/* TELEPORT */) {
      vnode.type.remove(vnode, internals)
    }

    // 移除自身节点
    if (doRemove) {
      remove(vnode)
    }
  }
  if ((vnodeHook = props && props.onVnodeUnmounted) || shouldInvokeDirs) {
    queuePostRenderEffect(() => {
      vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)

      // 在移除元素的子节点之后执行指令的 unmounted 钩子
      if (shouldInvokeDirs) {
        invokeDirectiveHook(vnode, null, parentComponent, 'unmounted')
      }
    }, parentSuspense)
  }
}