写在前面
尤大北京时间 9月18日 下午的时候发了一个微博,人狠话不多。看到这个表情,大家都知道有大事要发生。果然,在写这篇文章的时候,上 GitHub
上看了一眼,刚好碰上发布:
我们知道,一般开源软件的 release
就是一个 最终版本,看一下官方关于这个 release
版本的介绍:
更多关于这个 release
版本的信息可以关注:https://github.com/vuejs/vue-next/releases/tag/v3.0.0
除此之外,我在尤大的 GitHub
上发现了另一个东西 vue-lit,直觉告诉我这又是一个啥面向未来的下一代 xxx,所以我就点进去看了一眼是啥新玩具。
Hello World
看上去是尤大的一个验证性的尝试,看到 custom element
和 lit-html
,盲猜一把,是一个可以直接在浏览器中渲染 vue
写法的 Web Component
的工具。
按照尤大给的 Demo
,我们来试一下 Hello World
:
<!DOCTYPE html>
<html lang="en">
<head>
<script type="module">
import {
defineComponent,
reactive,
html,
onMounted
} from 'https://unpkg.com/@vue/[email protected]';
defineComponent('my-component', () => {
const state = reactive({
text: 'Hello World',
});
function onClick() {
alert('cliked!');
}
onMounted(() => {
console.log('mounted');
});
return () => html`
<p>
<button @click=${onClick}>Click me</button>
${state.text}
</p>
`;
})
</script>
</head>
<body>
<my-component />
</body>
</html>
不用任何编译打包工具,直接打开这个 index.html
,看上去没毛病:
!
可以看到,这里渲染出来的是一个 Web Component
,并且 mounted
生命周期也触发了。
关于 lit-html 和 lit-element
看 vue-lit
之前,我们先了解一下 lit-html
和 lit-ement
,这两个东西其实已经出来很久了,可能并不是所有人都了解。
lit-html
lit-html 可能很多人并不熟悉,甚至没有见过。
所以是啥?答案是 HTML 模板引擎。
如果没有体感,我问一个问题,React
核心的东西有哪些?大家都会回答:jsx
、Virtual-DOM
、diff
,没错,就是这些东西构成了 UI = f(data)
的 React
。
来看看 jsx
的语法:
function App() {
const msg = 'Hello World';
return <div>{msg}</div>;
}
再看看 lit-html
的语法:
function App() {
const msg = 'Hello World';
return html`
<div>${msg}</div>
`;
}
我们知道 jsx
是需要编译的它的底层最终还是 createElement
....。而 lit-html
就不一样了,它是基于 tagged template
的,使得它不用编译就可以在浏览器上运行,并且和 HTML Template
结合想怎么玩怎么玩,扩展能力更强,不香吗?
当然,无论是 jsx
还是 lint-html
,这个 App
都是需要 render
到真实 DOM
上。
lint-html 实现一个 Button 组件
直接上代码(省略样式代码):
<!DOCTYPE html>
<html lang="en">
<head>
<script type="module">
import { html, render } from 'https://unpkg.com/lit-html?module';
const Button = (text, props = {
type: 'default',
borderRadius: '2px'
}, onClick) => {
// 点击事件
const clickHandler = {
handleEvent(e) {
alert('inner clicked!');
if (onClick) {
onClick();
}
},
capture: true,
};
return html`
<div class="btn btn-${props.type}" @click=${clickHandler}>
${text}
</div>
`
};
render(Button('Defualt'), document.getElementById('button1'));
render(Button('Primary', { type: 'primary' }, () => alert('outer clicked!')), document.getElementById('button2'));
render(Button('Error', { type: 'error' }), document.getElementById('button3'));
</script>
</head>
<body>
<div id="button1"></div>
<div id="button2"></div>
<div id="button3"></div>
</body>
</html>
效果:
性能
lit-html
会比 React
性能更好吗?这里我没仔细看过源码,也没进行过相关实验,无法下定论。
但是可以大胆猜测一下,lit-html
没有使用类 diff
算法而是直接基于相同 template
的更新,看上去这种方式会更轻量一点。
但是,我们常问的一个问题 “在渲染列表的时候,key 有什么用?”,这个在 lit-html
是不是没法解决了。我如果删除了长列表中的其中一项,按照 lit-html
的基于相同 template
的更新,整个长列表都会更新一次,这个性能就差很多了啊。
lit-element
lit-element 这又是啥呢?
关键词:web components。
例子:
import { LitElement, html } from 'lit-element';
class MyElement extends LitElement {
static get properties() {
return {
msg: { type: String },
};
}
constructor() {
super();
this.msg = 'Hello World';
}
render() {
return html`
<p>${this.msg}</p>
`;
}
}
customElements.define('my-element', MyElement);
效果:
结论:可以用类 React
的语法写 Web Component
。
so, lit-element
是一个可以创建 Web Component
的 base class
。分析一下上面的 Demo,lit-element
做了什么事情:
- static get properties: 可以
setter
的state
- constructor: 初始化
state
- render: 通过
lit-html
渲染元素,并且会创建ShadowDOM
总之,lit-element
遵守 Web Components
标准,它是一个 class
,基于它可以快速创建 Web Component
。
更多关于如何使用 lit-element
进行开发,在这里就不展开说了。
Web Components
浏览器原生能力香吗?
说 Web Components
之前我想先问问大家,大家还记得 jQuery
吗,它方便的选择器让人难忘。但是后来 document.querySelector
这个 API
的出现并且广泛使用,大家似乎就慢慢地淡忘了 jQuery
。
浏览器原生 API
已经足够好用,我们并不需要为了操作 DOM
而使用 jQuery
。
再后来,是不是很久没有直接操作过 DOM
了?
是的,由于 React
/ Vue
等框架(库)的出现,帮我们做了很多事情,我们可以不用再通过复杂的 DOM API
来操作 DOM
。
我想表达的是,是不是有一天,如果浏览器原生能力足够好用的时候,React
等是不是也会像 jQuery
一样被浏览器原生能力替代?
组件化
像 React
/ Vue
等框架(库)都做了同样的事情,在之前浏览器的原生能力是实现不了的,比如创建一个可复用的组件,可以渲染在 DOM
中的任意位置。
现在呢?我们似乎可以不使用任意的框架和库,甚至不用打包编译,仅是通过 Web Components
这样的浏览器原生能力就可以创建可复用的组件,是不是未来的某一天我们就抛弃了现在所谓的框架和库,直接使用原生 API
或者是使用基于 Web Components
标准的框架和库来开发了?
我不是一个 Web Components 的无脑吹,只不过,我们需要面向未来编程。
来看看 Web Components
的一些主要功能吧。
Custom elements: 自定义元素
自定义元素顾名思义就是用户可以自定义 HTML
元素,通过 CustomElementRegistry
的 define
来定义,比如:
window.customElements.define('my-element', MyElement);
然后就可以直接通过 <my-element />
使用了。
根据规范,有两种 Custom elements
:
- Autonomous custom elements: 独立的元素,不继承任何
HTML
元素,使用时可以直接<my-element />
- Customized buld-in elements: 继承自
HTML
元素,比如通过{ extends: 'p' }
来标识继承自p
元素,使用时需要<p is="my-element"></p>
两种 Custom elements
在实现的时候也有所区别:
// Autonomous custom elements
class MyElement extends HTMLElement {
constructor() {
super();
}
}
// Customized buld-in elements:继承自 p 元素
class MyElement extends HTMLParagraphElement {
constructor() {
super();
}
}
生命周期函数
在 Custom elements
的构造函数中,可以指定多个回调函数,它们将会在元素的不同生命时期被调用。
- connectedCallback:元素首次被插入文档
DOM
时 - disconnectedCallback:元素从文档
DOM
中删除时 - adoptedCallback:元素被移动到新的文档时
- attributeChangedCallback: 元素增加、删除、修改自身属性时
我们这里留意一下 attributeChangedCallback
,是每当元素的属性发生变化时,就会执行这个回调函数,并且获得元素的相关信息:
attributeChangedCallback(name, oldValue, newValue) {
// TODO
}
需要特别注意的是,如果需要在元素某个属性变化后,触发 attributeChangedCallback()
回调函数,你必须监听这个属性:
class MyElement extends HTMLElement {
static get observedAttributes() {
return ['my-name'];
}
constructor() {
super();
}
}
元素的 my-name
属性发生变化时,就会触发回调方法。
Shadow DOM
Web Components
一个非常重要的特性,可以将结构、样式封装在组件内部,与页面上其它代码隔离,这个特性就是通过 Shadow DOM
实现。
关于 Shadow DOM
,这里主要想说一下 CSS
样式隔离的特性。Shadow DOM
里外的 selector
是相互获取不到的,所以也没办法在内部使用外部定义的样式,当然外部也没法获取到内部定义的样式。
这样有什么好处呢?划重点,样式隔离,Shadow DOM
通过局部的 HTML
和 CSS
,解决了样式上的一些问题,类似 vue
的 scope
的感觉,元素内部不用关心 selector
和 CSS rule
会不会被别人覆盖了,会不会不小心把别人的样式给覆盖了。所以,元素的 selector
非常简单:title
/ item
等,不需要任何的工具或者命名的约束。
Templates: 模板
可以通过 <template>
来添加一个 Web Component
的 Shadow DOM
里的 HTML
内容:
<body>
<template id="my-paragraph">
<style>
p {
color: white;
background-color: #666;
padding: 5px;
}
</style>
<p>My paragraph</p>
</template>
<script>
customElements.define('my-paragraph',
class extends HTMLElement {
constructor() {
super();
let template = document.getElementById('my-paragraph');
let templateContent = template.content;
const shadowRoot = this.attachShadow({mode: 'open'}).appendChild(templateContent.cloneNode(true));
}
}
)
</script>
<my-paragraph></my-paragraph>
</body>
效果:
我们知道,<template>
是不会直接被渲染的,所以我们是不是可以定义多个 <template>
然后在自定义元素时根据不同的条件选择渲染不同的 <template>
?答案当然是:可以。
vue-lit
介绍了 lit-html/element
和 Web Components
,我们回到尤大这个 vue-lit
。
首先我们看到在 Vue 3.0
的 Release
里有这么一段:
意思大概就是说 @vue/reactivity
模块和类似 lit-html
的方案配合,也能设计出一个直接访问 Vue
响应式系统的解决方案。
巧了不是,对上了,这不就是 vue-lit
吗?
源码解析
import { render } from 'https://unpkg.com/lit-html?module'
import {
shallowReactive,
effect
} from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'
lit-html
提供核心render
能力@vue/reactiity
提供Vue
响应式系统的能力
这里稍带解释一下 shallowReactive
和 effect
,不展开:
shallowReactive:简单理解就是“浅响应”,类似于“浅拷贝”,它仅仅是响应数据的第一层
const state = shallowReactive({
a: 1,
b: {
c: 2,
},
})
state.a++ // 响应式
state.b.c++ // 非响应式
effect:简单理解就是 watcher
const state = reactive({
name: "前端试炼",
});
console.log(state); // 这里返回的是Proxy代理后的对象
effect(() => {
console.log(state.name); // 每当name数据变化将会导致effect重新执行
});
接着往下看:
export function defineComponent(name, propDefs, factory) {
// propDefs
// 如果是函数,则直接当作工厂函数
// 如果是数组,则监听他们,触发 attributeChangedCallback 回调函数
if (typeof propDefs === 'function') {
factory = propDefs
propDefs = []
}
// 调用 Web Components 创建 Custom Elements 的函数
customElements.define(
name,
class extends HTMLElement {
// 监听 propDefs
static get observedAttributes() {
return propDefs
}
constructor() {
super()
// 创建一个浅响应
const props = (this._props = shallowReactive({}))
currentInstance = this
const template = factory.call(this, props)
currentInstance = null
// beforeMount 生命周期
this._bm && this._bm.forEach((cb) => cb())
// 定义一个 Shadow root,并且内部实现无法被 JavaScript 访问及修改,类似 <video> 标签
const root = this.attachShadow({ mode: 'closed' })
let isMounted = false
// watcher
effect(() => {
if (!isMounted) {
// beforeUpdate 生命周期
this._bu && this._bu.forEach((cb) => cb())
}
// 调用 lit-html 的核心渲染能力,参考上文 lit-html 的 Demo
render(template(), root)
if (isMounted) {
// update 生命周期
this._u && this._u.forEach((cb) => cb())
} else {
// 渲染完成,将 isMounted 置为 true
isMounted = true
}
})
}
connectedCallback() {
// mounted 生命周期
this._m && this._m.forEach((cb) => cb())
}
disconnectedCallback() {
// unMounted 生命周期
this._um && this._um.forEach((cb) => cb())
}
attributeChangedCallback(name, oldValue, newValue) {
// 每次修改 propDefs 里的参数都会触发
this._props[name] = newValue
}
}
)
}
// 挂载生命周期
function createLifecycleMethod(name) {
return (cb) => {
if (currentInstance) {
;(currentInstance[name] || (currentInstance[name] = [])).push(cb)
}
}
}
// 导出生命周期
export const onBeforeMount = createLifecycleMethod('_bm')
export const onMounted = createLifecycleMethod('_m')
export const onBeforeUpdate = createLifecycleMethod('_bu')
export const onUpdated = createLifecycleMethod('_u')
export const onUnmounted = createLifecycleMethod('_um')
// 导出 lit-hteml 和 @vue/reactivity 的所有 API
export * from 'https://unpkg.com/lit-html?module'
export * from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'
简化版有助于理解
整体看下来,为了更好地理解,我们不考虑生命周期之后可以简化一下:
import { render } from 'https://unpkg.com/lit-html?module'
import {
shallowReactive,
effect
} from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'
export function defineComponent(name, factory) {
customElements.define(
name,
class extends HTMLElement {
constructor() {
super()
const root = this.attachShadow({ mode: 'closed' })
effect(() => {
render(factory(), root)
})
}
}
)
}
也就这几个流程:
- 创建
Web Components
的Custom Elements
- 创建一个
Shadow DOM
的ShadowRoot
节点 - 将传入的
factory
和内部创建的ShadowRoot
节点交给lit-html
的render
渲染出来
回过头来看尤大提供的 DEMO:
import {
defineComponent,
reactive,
html,
} from 'https://unpkg.com/@vue/lit'
defineComponent('my-component', () => {
const msg = 'Hello World'
const state = reactive({
show: true
})
const toggle = () => {
state.show = !state.show
}
return () => html`
<button @click=${toggle}>toggle child</button>
${state.show ? html`<my-child msg=${msg}></my-child>` : ``}
`
})
my-component
是传入的 name
,第二个是一个函数,也就是传入的 factory
,其实就是 lit-html
的第一个参数,只不过引入了 @vue/reactivity
的 reactive
能力,把 state
变成了响应式。
没毛病,和 Vue 3.0 Release
里说的一致,@vue/reactivity
可以和 lit-html
配合,使得 Vue
和 Web Components
结合到一块儿了,是不是还挺有意思。
写在最后
可能尤大只是一时兴起,写了这个小玩具,但是可以见得这可能真的是一种大趋势。
猜测不久将来这些关键词会突然就爆发:Unbundled
/ ES Modules
/ Web components
/ Custom Element
/ Shadow DOM
...
是不是值得期待一下?
思考可能还比较浅,文笔有限,不足之处欢迎大家指出。
更多文章
可以关注公众号「前端试炼」,或者加微信好友 qianduanshilian,加入交流群。