尤大 3 天前發在 GitHub 上的 vue-lit 是啥?


寫在前面

尤大北京時間 9月18日 下午的時候發了一個微博,人狠話很少。看到這個表情,你們都知道有大事要發生。果真,在寫這篇文章的時候,上 GitHub 上看了一眼,恰好碰上發佈:html

咱們知道,通常開源軟件的 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 elementlit-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,看上去沒毛病:

能夠看到,這裏渲染出來的是一個 Web Component,而且 mounted 生命週期也觸發了。

介紹 vue-lit 以前,咱們須要先有一些前置知識。

關於 lit-html 和 lit-element

vue-lit 以前,咱們先了解一下 lit-htmllit-ement,這兩個東西其實已經出來好久了,可能並非全部人都瞭解。

lit-html

lit-html[3] 可能不少人並不熟悉,甚至沒有見過。

因此是啥?答案是 HTML 模板引擎

若是沒有體感,我問一個問題,React 核心的東西有哪些?你們都會回答:jsxVirtual-DOMdiff,沒錯,就是這些東西構成了 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();
          }
        },
        capturetrue,
      };

      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 的更新,整個長列表都會更新一次,這個性能就差不少了啊。

// TODO:埋個坑,之後看

lit-element

lit-element[4] 這又是啥呢?

關鍵詞:web components

例子

import { LitElement, html } from 'lit-element';

class MyElement extends LitElement {
  static get properties() {
    return {
      msg: { typeString },
    };
  }
  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 Componentbase class。分析一下上面的 Demo,lit-element 作了什麼事情:

  1. static get properties: 能夠 setterstate
  2. constructor: 初始化 state
  3. 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 元素,經過 CustomElementRegistrydefine 來定義,好比:

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 實現。

關於 Shadow DOM,這裏主要想說一下 CSS 樣式隔離的特性。Shadow DOM 裏外的 selector 是相互獲取不到的,因此也沒辦法在內部使用外部定義的樣式,固然外部也無法獲取到內部定義的樣式。

這樣有什麼好處呢?劃重點,樣式隔離,Shadow DOM 經過局部的 HTMLCSS,解決了樣式上的一些問題,相似 vuescope 的感受,元素內部不用關心 selectorCSS rule 會不會被別人覆蓋了,會不會不當心把別人的樣式給覆蓋了。因此,元素的 selector 很是簡單:title / item 等,不須要任何的工具或者命名的約束。

更多關於 Shadow DOM[7]

Templates: 模板

能夠經過 <template> 來添加一個 Web ComponentShadow DOM 裏的 HTML 內容:

<body>
  <template id="my-paragraph">
    <style>
      p {
        color: white;
        background-color#666;
        padding5px;
      }
    
</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>?答案固然是:能夠。

更多關於 Templates[8]

vue-lit

介紹了 lit-html/elementWeb Components,咱們回到尤大這個  vue-lit

首先咱們看到在 Vue 3.0Release 裏有這麼一段:

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 響應式系統的能力

這裏稍帶解釋一下 shallowReactiveeffect,不展開:

shallowReactive:簡單理解就是「淺響應」,相似於「淺拷貝」,它僅僅是響應數據的第一層

const state = shallowReactive({
  a1,
  b: {
    c2,
  },
})

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)
        })
      }
    }
  )
}

也就這幾個流程:

  1. 建立 Web ComponentsCustom Elements
  2. 建立一個 Shadow DOMShadowRoot 節點
  3. 將傳入的 factory 和內部建立的 ShadowRoot 節點交給 lit-htmlrender 渲染出來

回過頭來看尤大提供的 DEMO:

import {
  defineComponent,
  reactive,
  html,
from 'https://unpkg.com/@vue/lit'

defineComponent('my-component', () => {
  const msg = 'Hello World'
  const state = reactive({
    showtrue
  })
  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/reactivityreactive 能力,把 state 變成了響應式。

沒毛病,和 Vue 3.0 Release 裏說的一致,@vue/reactivity 能夠和 lit-html 配合,使得 VueWeb Components 結合到一起了,是否是還挺有意思。

寫在最後

可能尤大隻是一時興起,寫了這個小玩具,可是能夠見得這可能真的是一種大趨勢。

猜想不久未來這些關鍵詞會忽然就爆發:Unbundled / ES Modules / Web components / Custom Element / Shadow DOM...

是否是值得期待一下?

參考資料

[1]

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


交流討論

歡迎關注公衆號「前端試煉」,公衆號平時會分享一些實用或者有意思的東西,發現代碼之美。專一深度和最佳實踐,但願打造一個高質量的公衆號。



公衆號後臺回覆「加羣」,拉你進交流面試羣。

若是你不想加羣,只是想加我也是能夠的。

若是以爲這篇文章還不錯,來個【分享、點贊、在看】三連吧,讓更多的人也看到~

本文分享自微信公衆號 - 前端試煉(code-photo)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索