寫在前面
![](http://static.javashuo.com/static/loading.gif)
尤大北京時間 9月18日 下午的時候發了一個微博,人狠話很少。看到這個表情,你們都知道有大事要發生。果真,在寫這篇文章的時候,上 GitHub
上看了一眼,恰好碰上發佈:html
![](http://static.javashuo.com/static/loading.gif)
咱們知道,通常開源軟件的 release
就是一個 最終版本,看一下官方關於這個 release
版本的介紹:前端
Today we are proud to announce the official release of Vue.js 3.0 "One Piece".vue
更多關於這個 release
版本的信息能夠關注:https://github.com/vuejs/vue-next/releases/tag/v3.0.0[1]react
除此以外,我在尤大的 GitHub
上發現了另外一個東西 vue-lit[2],直覺告訴我這又是一個啥面向將來的下一代 xxx,因此我就點進去看了一眼是啥新玩具。ios
這篇文章就圍繞 vue-lit 展開說說。c++
Hello World
Proof of concept mini custom elements framework powered by @vue/reactivity and lit-html.git
首先,vue-lit 看上去是尤大的一個驗證性的嘗試,看到 custom element
和 lit-html
,盲猜一把,是一個能夠直接在瀏覽器中渲染 vue
寫法的 Web Component
的工具。github
這裏提到了
lit-html
,後面會專門介紹一下。web
按照尤大給的 Demo
,咱們來試一下 Hello World
:面試
<!DOCTYPE html>
<html lang="en">
<head>
<script type="module">
import {
defineComponent,
reactive,
html,
onMounted
} from 'https://unpkg.com/@vue/lit@0.0.2';
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
,看上去沒毛病:
![](http://static.javashuo.com/static/loading.gif)
能夠看到,這裏渲染出來的是一個 Web Component
,而且 mounted
生命週期也觸發了。
介紹 vue-lit 以前,咱們須要先有一些前置知識。
關於 lit-html 和 lit-element
看 vue-lit
以前,咱們先了解一下 lit-html
和 lit-ement
,這兩個東西其實已經出來好久了,可能並非全部人都瞭解。
lit-html
lit-html[3] 可能不少人並不熟悉,甚至沒有見過。
![](http://static.javashuo.com/static/loading.gif)
因此是啥?答案是 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>
效果:
![](http://static.javashuo.com/static/loading.gif)
性能
lit-html
會比 React
性能更好嗎?這裏我沒仔細看過源碼,也沒進行過相關實驗,沒法下定論。
可是能夠大膽猜想一下,lit-html
沒有使用類 diff
算法而是直接基於相同 template
的更新,看上去這種方式會更輕量一點。
可是,咱們常問的一個問題 「在渲染列表的時候,key 有什麼用?」,這個在 lit-html
是否是無法解決了。我若是刪除了長列表中的其中一項,按照 lit-html
的基於相同 template
的更新,整個長列表都會更新一次,這個性能就差不少了啊。
// TODO:埋個坑,之後看
lit-element
lit-element[4] 這又是啥呢?
![](http://static.javashuo.com/static/loading.gif)
關鍵詞: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);
效果:
![](http://static.javashuo.com/static/loading.gif)
結論:能夠用類 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
。
You Dont Need jQuery[5]
再後來,是否是好久沒有直接操做過 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[6]
生命週期函數
在 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
實現。
![](http://static.javashuo.com/static/loading.gif)
關於 Shadow DOM
,這裏主要想說一下 CSS
樣式隔離的特性。Shadow DOM
裏外的 selector
是相互獲取不到的,因此也沒辦法在內部使用外部定義的樣式,固然外部也無法獲取到內部定義的樣式。
這樣有什麼好處呢?劃重點,樣式隔離,Shadow DOM
經過局部的 HTML
和 CSS
,解決了樣式上的一些問題,相似 vue
的 scope
的感受,元素內部不用關心 selector
和 CSS rule
會不會被別人覆蓋了,會不會不當心把別人的樣式給覆蓋了。因此,元素的 selector
很是簡單:title
/ item
等,不須要任何的工具或者命名的約束。
更多關於 Shadow DOM[7]
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>
效果:
![](http://static.javashuo.com/static/loading.gif)
咱們知道,<template>
是不會直接被渲染的,因此咱們是否是能夠定義多個 <template>
而後在自定義元素時根據不一樣的條件選擇渲染不一樣的 <template>
?答案固然是:能夠。
更多關於 Templates[8]
vue-lit
介紹了 lit-html/element
和 Web Components
,咱們回到尤大這個 vue-lit
。
首先咱們看到在 Vue 3.0
的 Release
裏有這麼一段:
The @vue/reactivity module exports functions that provide direct access to Vue's reactivity system, and can be used as a standalone package. It can be used to pair with other templating solutions (e.g. lit-html) or even in non-UI scenarios.
意思大概就是說 @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
...
是否是值得期待一下?
參考資料
https://github.com/vuejs/vue-next/releases/tag/v3.0.0: https://github.com/vuejs/vue-next/releases/tag/v3.0.0
[2]vue-lit: https://github.com/yyx990803/vue-lit
[3]lit-html: https://lit-html.polymer-project.org/
[4]lit-element: https://lit-element.polymer-project.org/
[5]You Dont Need jQuery: https://github.com/nefe/You-Dont-Need-jQuery
[6]更多關於 Custom elements: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements
[7]更多關於 Shadow DOM: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM
[8]更多關於 Templates: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots
交流討論
歡迎關注公衆號「前端試煉」,公衆號平時會分享一些實用或者有意思的東西,發現代碼之美。專一深度和最佳實踐,但願打造一個高質量的公衆號。
![](http://static.javashuo.com/static/loading.gif)
公衆號後臺回覆「加羣」,拉你進交流面試羣。
若是你不想加羣,只是想加我也是能夠的。
若是以爲這篇文章還不錯,來個【分享、點贊、在看】三連吧,讓更多的人也看到~
本文分享自微信公衆號 - 前端試煉(code-photo)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。