vue3
细节变化(一)
包括以下几个内容:
-
组件data选项总是声明为函数
-
自定义组件白名单
-
is
属性仅限于用在component
标签上
组件data选项总是声明为函数
在vue2
中,data选项,若是创建vue
实例的时候传递为对象,若在组件内,则为函数形式
declare type ComponentOptions = {
data: Object | Function | void;
...
}
vue3
中data
选项统一为函数形式,返回响应式数据
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>
会被作为无效的内容提升到外部,并导致最终渲染结果出错
通过给tr 标签添加is
属性,骗过浏览器,绕过限制
<table>
<tr is="row" v-for="item in items" :data="item"></tr>
</table>
以上是is
属性的缘来,vue3
中,场景还是一样,但使用方式有所变化:
-
is
属性仅能用于动态组件component
标签上<component :is="comp"></component>
-
in-dom
模板解析使用v-is
代替<table> <tr v-is="row"></tr> </table>
思考
-
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 } ...
相对于
vue2
,vue3
代码上更加明确两种情况,编译的时候识别tag为component,获取判断是否有is
属性,有则为动态组件,其他tag不需要做此判断了,若有v-is
属性,在转换 的过程中也是直接跳过,然后具体的实现就在自定义指令中取实现。代码逻辑更加清晰简洁,相比于vue2 无脑的检查所有el,vue3的效率更高。
vue3
细节变化(二)
自定义指令API和组件保持一致
vue3
中指定api和组件保持一致,具体表现在:
vue2 | vue3 |
---|---|
bind | **beforeMount **当指令第一次绑定到元素并且在挂载父组件之前调用 |
inserted | mounted 在挂载绑定元素的父组件时调用 |
-- | **beforeUpdate **新增,元素自身更新前调用,和组件生命周期钩子很像 |
update | 移除, 使用updated 代替, 在包含组件的VNode 及其子组件的VNode 更新后调用 |
componentUpdated | updated |
-- | **beforeUnmount **新增,和组件生命周期钩子相似,元素将要被移除之前调用 |
unbind | unmounted |
自定义指令的钩子函数与组件保持一致,便于开发者使用,其他的用法基本上与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
示例:
<!-- 淡入淡出 -->
<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" />
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))