vue3学习笔记之应用变化(二)

616 阅读4分钟

vue3 细节变化(一)

包括以下几个内容:

  1. 组件data选项总是声明为函数

  2. 自定义组件白名单

  3. is 属性仅限于用在component 标签上

组件data选项总是声明为函数

vue2 中,data选项,若是创建vue实例的时候传递为对象,若在组件内,则为函数形式

declare type ComponentOptions = {
	 data: Object | Function | void;
	 ...
}

vue3data 选项统一为函数形式,返回响应式数据

createApp({
	data() {
		return {
			xx: '12345'
		}
	}
}).mount('#app')

自定义组件白名单

当自定义渲染器时,使用自定义组件会报警告

[Vue warn]: Failed to resolve component: custom-el at <App>

vue3 中自定义元素检测发生在模板编译时,所以如果要添加一些vue之外的自定义元素,需要在编译器选项中设置isCustomElement选项。

使用vue-cli构建工具时,模板都会用vue-loader 预编译,设置提供的compilerOptions即可:

rules: {
	{
		test: /\.vue$/,
		use: 'vue-loader',
		options: {
            compierOptions: {
				isCustomElement: tag => tag === 'custom-el'
            }
		}
	}
}

使用vite构建项目,在vite.config.js 中配置vueCompilerOptions

isCustomElement: tag => tag === 'custom-button'moudulexports = {
	vueCompilerOptions: {
		isCustomElement: tag => tag === 'custom-el' 
	}
}

is 属性仅限于用在component 标签上

vue2中,is属性主要用于一下两种场景:

  • 动态组件:实现不同组件之间进行动态切换
<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component :is="comp"></component>
  • 解决in-dom场景下解析dom模板时遇到的限制

    限制:有些html 元素,如<ul><ol><table><select>,对于内部可以放哪些元素有严格限制,而有些元素,如<li><tr><option>,只能出现在其它某些特定的元素内部。

    当在in-dom模板上()就会遇到上述限制

    in-dom模板,即直接在html上编写模板。字符串模板(template: '...'), 单文件组件(.vue 文件), <script type="text/x-template"> 不受上述限制

    	<div id="app">
            <table>
                <row v-for="item in items" :data="item"></row>
            </table>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
        <script>
            new Vue({
                data: {
                    items: ['aaa', 'bbb']
                },
                components: {
                    row: {
                        props: ['data'], 
                        template: "<tr><td>{{this.data}}</td></tr>"
                    }
                }
            }).$mount('#app');
        </script>
    

    渲染时,<row>会被作为无效的内容提升到外部,并导致最终渲染结果出错

image-20210428142656080.png

​ 通过给tr 标签添加is属性,骗过浏览器,绕过限制

    <table>
        <tr is="row" v-for="item in items" :data="item"></tr>
    </table>

以上是is 属性的缘来,vue3 中,场景还是一样,但使用方式有所变化:

  1. is 属性仅能用于动态组件component标签上

     <component :is="comp"></component>
    
  2. in-dom模板解析使用v-is 代替

     <table>
     	<tr v-is="row"></tr>
     </table>
    
思考
  1. in-dom模板解析改为使用v-is 代替,有何好处?原先使用is属性存在什么问题?

    vue2 is属性相关源码:

    let maybeComponent
    
    // 创建AST
    export function createASTElement (
      tag: string,
      attrs: Array<ASTAttr>,
      parent: ASTElement | void
    ): ASTElement {
      return {
        type: 1,
        tag,
        attrsList: attrs,
        attrsMap: makeAttrsMap(attrs),  // 存放el 属性的map
        rawAttrsMap: {},
        parent,
        children: []
      }
    }
    
    // 获取绑定的属性值
    export function getAndRemoveAttr (
      el: ASTElement,
      name: string,
      removeFromMap?: boolean
    ): ?string {
      let val
      if ((val = el.attrsMap[name]) != null) {
        const list = el.attrsList
        for (let i = 0, l = list.length; i < l; i++) {
          if (list[i].name === name) {
            list.splice(i, 1)
            break
          }
        }
      }
      if (removeFromMap) {
        delete el.attrsMap[name]
      }
      return val
    }
    
    // 如果is属性,则将is属性绑定的组件 赋值到 el.component
    function processComponent (el) {
      let binding
      if ((binding = getBindingAttr(el, 'is'))) {
        el.component = binding
      }
      if (getAndRemoveAttr(el, 'inline-template') != null) {
        el.inlineTemplate = true
      }
    }
    /**
     * Convert HTML string to AST.
     */
    export function parse (
      template: string,
      options: CompilerOptions
    ): ASTElement | void {
      ...
        // 判断传入的el是否存在
      maybeComponent = (el: ASTElement) => !!(
        el.component ||          // 组件
        el.attrsMap[':is'] ||        // is绑定的组件
        el.attrsMap['v-bind:is'] ||
        !(el.attrsMap.is ? isReservedTag(el.attrsMap.is) : isReservedTag(el.tag)) // 标签
      )
      ....
        function closeElement (element) {
            trimEndingWhitespace(element)
            if (!inVPre && !element.processed) {
              element = processElement(element, options)
            }
            ...
        }
    }
    

    个人理解::HTML转为AST时候,识别每个el节点上是否有is 属性,若有,则将该属性对应的组件赋值给el.component ,替换当前元素。

    vue3 is`属性相关源码

    // compiler-core/transfromElement.ts
    function isComponentTag(tag: string) {
      return tag[0].toLowerCase() + tag.slice(1) === 'component'
    }
    for (let i = 0; i < props.length; i++) {
    ...
        // skip :is on <component>
        if (name === 'is' && isComponentTag(tag)) {
        	continue
        }
        ...
        // skip v-is and :is on <component>
        if (name === 'is' ||(isBind && isComponentTag(tag) && isBindKey(arg, 'is'))) {
        	continue
        }
    }
    
    // compiler-core/parse.ts
    ...
       const hasVIs = props.some(
          p => p.type === NodeTypes.DIRECTIVE && p.name === 'is'
        )
        if (options.isNativeTag && !hasVIs) {
          if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT
        } else if (
          hasVIs ||
          isCoreComponent(tag) ||
          (options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
          /^[A-Z]/.test(tag) ||
          tag === 'component'
        ) {
          tagType = ElementTypes.COMPONENT
        }
    ...
    

    相对于vue2vue3 代码上更加明确两种情况,编译的时候识别tag为component,获取判断是否有is属性,有则为动态组件,其他tag不需要做此判断了,若有v-is属性,在转换 的过程中也是直接跳过,然后具体的实现就在自定义指令中取实现。

    代码逻辑更加清晰简洁,相比于vue2 无脑的检查所有el,vue3的效率更高。

vue3 细节变化(二)

自定义指令API和组件保持一致

vue3 中指定api和组件保持一致,具体表现在:

vue2vue3
bind**beforeMount**当指令第一次绑定到元素并且在挂载父组件之前调用
insertedmounted 在挂载绑定元素的父组件时调用
--**beforeUpdate**新增,元素自身更新前调用,和组件生命周期钩子很像
update移除, 使用updated代替, 在包含组件的VNode 及其子组件的VNode 更新后调用
componentUpdatedupdated
--**beforeUnmount**新增,和组件生命周期钩子相似,元素将要被移除之前调用
unbindunmounted

自定义指令的钩子函数与组件保持一致,便于开发者使用,其他的用法基本上与vue2 一致

示例:

// 点击元素外部触发事件的自定义指令

const on = (function () {
    if (document.addEventListener) {
      return function (element, event, handler) {
        if (element && event && handler) {
          element.addEventListener(event, handler, false)
        }
      }
    } else {
      return function (element, event, handler) {
        if (element && event && handler) {
          element.attachEvent('on' + event, handler)
        }
      }
    }
  })()

const nodeList = []
const ctx = '@@clickoutsideContext'

let startClick
let seed = 0

// !Vue.prototype.$isServer && on(document, 'mousedown', e => (startClick = e))
on(document, 'mousedown', (e) => (startClick = e))

on(document, 'mouseup', (e) => {
  // !Vue.prototype.$isServer && on(document, 'mouseup', e => {
  nodeList.forEach((node) => node[ctx].documentHandler(e, startClick))
})

function createDocumentHandler(el, binding, vnode) {
  return function (mouseup = {}, mousedown = {}) {
    if (
      !vnode ||
      !binding.instance ||
      !mouseup.target ||
      !mousedown.target ||
      el.contains(mouseup.target) ||
      el.contains(mousedown.target) ||
      el === mouseup.target ||
      (binding.instance.popperElm &&
        (binding.instance.popperElm.contains(mouseup.target) ||
          binding.instance.popperElm.contains(mousedown.target)))
    )
      return

    if (
      binding.expression &&
      el[ctx].methodName &&
      binding.instance[el[ctx].methodName]
    ) {
      binding.instance[el[ctx].methodName]()
    } else {
      el[ctx].bindingFn && el[ctx].bindingFn()
    }
  }
}

/**
 * v-clickoutside
 * @desc 点击元素外面才会触发的事件
 * @example
 * ```vue
 * <div v-clickoutside="handleClose">
 * ```
 */
createApp(App)
.directive('clickoutside', {
  beforeMount(el, binding, vnode) {
    nodeList.push(el)
    const id = seed++
    el[ctx] = {
      id,
      documentHandler: createDocumentHandler(el, binding, vnode),
      methodName: binding.expression,
      bindingFn: binding.value
    }
  },

  updated(el, binding, vnode) {
    el[ctx].documentHandler = createDocumentHandler(el, binding, vnode)
    el[ctx].methodName = binding.expression
    el[ctx].bindingFn = binding.value
  },

  unmounted(el) {
    const len = nodeList.length

    for (let i = 0; i < len; i++) {
      if (nodeList[i][ctx].id === el[ctx].id) {
        nodeList.splice(i, 1)
        break
      }
    }
    delete el[ctx]
  })
.mount('#app')

vue3 细节变化(三)

transition类名变更:
  • v-enter ----> v-enter-from

  • v-leave ----> v-leave-from

image-20210428231056132.png 示例:

<!-- 淡入淡出 -->
<template>
  <div id="demo">
    <button @click="show = !show">Toggle</button>
    <transition name="fade">
      <p v-if="show">hello</p>
    </transition>
  </div>
</template>
<script>
export default {
  data() {
    return {
      show: true,
    };
  },
};
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

组件watch 选项和实例方法$watch 不再支持点分隔符字符串路径

以 . 分割的表达式不再被watch$watch支持,可以使用计算函数作为参数实现

this.$watch(() => this.foo.bar, (v1, v2) => {
	console.log(this.foo.bar)
})

TODO: 测试发现vue3中watch选项使用点分隔符字符串路径正常,不知道是不是理解上有偏差

Vue2.x 中应用程序根容器的outerHTML 会被根组件的模版替换(或被编译成template),vue3.x现在使用根容器的innerHTML取代
// 根容器解析-vue2相关代码
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}
/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}
// 根容器解析-vue3
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)

  if (__DEV__) {
    injectNativeTagCheck(app)
    injectCustomElementCheck(app)
  }

  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
    const component = app._component
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML
    }
    // clear content before mounting
    container.innerHTML = ''
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

vue3 细节变化(四)

keyCode 作为 v-on 修饰符被移除

vue2中可以使用keyCode 指代某个按键,可读性差,vue3 不再支持,

<!-- keyCode 方式不再被支持 -->
<input v-on:keyup.13="submit" />

<!-- 只能使用alias方式 -->
<input v-on:keyup.enter="submit" />

onon,off , $once 被移除

这3个方法被认为不应该由vue 提供,因此被移除,可以使用其他第三方库实现,官方推荐mitt,相当于vue2中的eventBus使用

yarn add mitt -S

import mitt from 'mitt';

// 创建emitter
const emitter = mitt();

//发送事件
emitter.emit('someEvent', 'fooo');

// 监听事件
emitter.on('someEvent', msg => console.log(msg))