新聞中心
基于 JSX 的動(dòng)態(tài)數(shù)據(jù)綁定歸屬于筆者的 React 與前端工程化實(shí)踐中的,本文中設(shè)計(jì)的引用資料參考 React 學(xué)習(xí)與實(shí)踐資料索引,如果有對(duì) JavaScript 基礎(chǔ)語(yǔ)法尚存疑惑的可以參閱現(xiàn)代 JavaScript 開(kāi)發(fā):語(yǔ)法基礎(chǔ)與實(shí)踐技巧。

創(chuàng)新互聯(lián)公司2013年成立,是專(zhuān)業(yè)互聯(lián)網(wǎng)技術(shù)服務(wù)公司,擁有項(xiàng)目成都網(wǎng)站制作、網(wǎng)站建設(shè)、外貿(mào)網(wǎng)站建設(shè)網(wǎng)站策劃,項(xiàng)目實(shí)施與項(xiàng)目整合能力。我們以讓每一個(gè)夢(mèng)想脫穎而出為使命,1280元新樂(lè)做網(wǎng)站,已為上家服務(wù),為新樂(lè)各地企業(yè)和個(gè)人服務(wù),聯(lián)系電話:18982081108
基于 JSX 的動(dòng)態(tài)數(shù)據(jù)綁定
筆者在 2016-我的前端之路: 工具化與工程化一文中提及,前端社區(qū)用了 15 年的時(shí)間來(lái)分割 HTML、JavaScript 與 CSS,但是隨著 JSX 的出現(xiàn)仿佛事物一夕回到解放前。在 Angular、Vue.js 等 MVVM 前端框架中都是采用了指令的方式來(lái)描述業(yè)務(wù)邏輯,而 JSX 本質(zhì)上還是 JavaScript,即用 JavaScript 來(lái)描述業(yè)務(wù)邏輯。雖然 JSX 被有些開(kāi)發(fā)者評(píng)論為丑陋的語(yǔ)法,但是筆者還是秉持 JavaScript First 原則,盡可能地用 JavaScript 去編寫(xiě)業(yè)務(wù)代碼。在前文 React 初窺:JSX 詳解中我們探討了 JSX 的前世今生與基本用法,而本部分我們著手編寫(xiě)簡(jiǎn)單的面向 DOM 的 JSX 解析與動(dòng)態(tài)數(shù)據(jù)綁定庫(kù);本部分所涉及的代碼歸納于 Ueact 庫(kù)。
JSX 解析與 DOM 元素構(gòu)建
元素構(gòu)建
筆者在 JavaScript 語(yǔ)法樹(shù)與代碼轉(zhuǎn)化實(shí)踐 一文中介紹過(guò) Babel 的原理與用法,這里我們?nèi)匀皇褂?Babel 作為 JSX 語(yǔ)法解析工具;為了將 JSX 聲明轉(zhuǎn)化為 createElement 調(diào)用,這里需要在項(xiàng)目的 .babelrc 文件中做如下配置:
- "plugins": [
- "transform-decorators-legacy",
- "async-to-promises",
- [
- "transform-react-jsx", {
- "pragma": "createElement"
- }
- ]
- ],
這里的 createElement 函數(shù)聲明如下:
- /**
- * Description 從 JSX 中構(gòu)建虛擬 DOM
- * @param tagName 標(biāo)簽名
- * @param props 屬性
- * @param childrenArgs 子元素列表
- */
- export function createElement(
- tagName: string,
- props: propsType,
- ...childrenArgs: [any]
- ) {}
該函數(shù)包含三個(gè)參數(shù),分別指定標(biāo)簽名、屬性對(duì)象與子元素列表;實(shí)際上經(jīng)過(guò) Babel 的轉(zhuǎn)化之后,JSX 文本會(huì)成為如下的函數(shù)調(diào)用(這里還包含了 ES2015 其他的語(yǔ)法轉(zhuǎn)化):
- ...
- (0, _createElement.createElement)(
- 'section',
- null,
- (0, _createElement.createElement)(
- 'section',
- null,
- (0, _createElement.createElement)(
- 'button',
- { className: 'link', onClick: handleClick },
- 'Custom DOM JSX'
- ),
- (0, _createElement.createElement)('input', {
- type: 'text',
- onChange: function onChange(e) {
- console.log(e);
- }
- })
- )
- ),
- ...
在獲取到元素標(biāo)簽之后,我們首先要做的就是創(chuàng)建元素;創(chuàng)建元素 createElementByTag 過(guò)程中我們需要注意區(qū)分普通元素與 SVG 元素:
- export const createElementByTag = (tagName: string) => {
- if (isSVG(tagName)) {
- return document.createElementNS('http://www.w3.org/2000/svg', tagName);
- }
- return document.createElement(tagName);
- };
屬性處理
在創(chuàng)建了新的元素對(duì)象之后,我們需要對(duì) createElement 函數(shù)傳入的后續(xù)參數(shù)進(jìn)行處理,也就是為元素設(shè)置對(duì)應(yīng)的屬性;基本的屬性包含了樣式類(lèi)、行內(nèi)樣式、標(biāo)簽屬性、事件、子元素以及樸素的 HTML 代碼等。首先我們需要對(duì)子元素進(jìn)行處理:
- // 處理所有子元素,如果子元素為單純的字符串,則直接創(chuàng)建文本節(jié)點(diǎn)
- const children = flatten(childrenArgs).map(child => {
- // 如果子元素同樣為 Element,則創(chuàng)建該子元素的副本
- if (child instanceof HTMLElement) {
- return child;
- }
- if (typeof child === 'boolean' || child === null) {
- child = '';
- }
- return document.createTextNode(child);
- });
這里可以看出,對(duì) createElement 函數(shù)的執(zhí)行是自底向上執(zhí)行的,因此傳入的子元素參數(shù)實(shí)際上是已經(jīng)經(jīng)過(guò)渲染的 HTML 元素。接下來(lái)我們還需要對(duì)其他屬性進(jìn)行處理:
- ...
- // 同時(shí)支持 class 與 className 設(shè)置
- const className = props.class || props.className;
- // 如果存在樣式類(lèi),則設(shè)置
- if (className) {
- setAttribute(tagName, el, 'class', classNames(className));
- }
- // 解析行內(nèi)樣式
- getStyleProps(props).forEach(prop => {
- el.style.setProperty(prop.name, prop.value);
- });
- // 解析其他 HTML 屬性
- getHTMLProps(props).forEach(prop => {
- setAttribute(tagName, el, prop.name, prop.value);
- });
- // 設(shè)置事件監(jiān)聽(tīng),這里為了解決部分瀏覽器中異步問(wèn)題因此采用同步寫(xiě)法
- let events = getEventListeners(props);
- for (let event of events) {
- el[event.name] = event.listener;
- }
- ...
React 中還允許直接設(shè)置元素的內(nèi)部 HTML 代碼,這里我們也需要判斷是否存在有 dangerouslySetInnerHTML 屬性:
- // 如果是手動(dòng)設(shè)置 HTML,則添加 HTML,否則設(shè)置顯示子元素
- if (setHTML && setHTML.__html) {
- el.innerHTML = setHTML.__html;
- } else {
- children.forEach(child => {
- el.appendChild(child);
- });
- }
到這里我們就完成了針對(duì) JSX 格式的樸素的 DOM 標(biāo)簽轉(zhuǎn)化的 createElement 函數(shù),完整的源代碼參考這里。
簡(jiǎn)單使用
這里我們依舊使用 create-webpack-app 腳手架來(lái)搭建示例項(xiàng)目,這里我們以簡(jiǎn)單的計(jì)數(shù)器為例描述其用法。需要注意的是,本部分尚未引入雙向數(shù)據(jù)綁定,或者說(shuō)是自動(dòng)狀態(tài)變化更新,還是使用的樸素的 DOM 選擇器查詢更新方式:
- // App.js
- import { createElement } from '../../../src/dom/jsx/createElement';
- // 頁(yè)面內(nèi)狀態(tài)
- const state = {
- count: 0
- };
- /**
- * Description 點(diǎn)擊事件處理
- * @param e
- */
- const handleClick = e => {
- state.count++;
- document.querySelector('#count').innerText = state.count;
- };
- export default (
- Custom DOM JSX
- onChange={(e)=>{
- console.log(e);
- }}
- />
- {state.count}
- );
- // client.js
- // @flow
- import App from './component/Count';
- document.querySelector('#root').appendChild(App);
數(shù)據(jù)綁定
當(dāng)我們使用 Webpack 在后端編譯 JSX 時(shí),會(huì)將其直接轉(zhuǎn)化為 JavaScript 中函數(shù)調(diào)用,因此可以自然地在作用域中聲明變量然后在 JSX 中直接引用;不過(guò)筆者在設(shè)計(jì) Ueact 時(shí)考慮到,為了方便快速上手或者簡(jiǎn)單的 H5 頁(yè)面開(kāi)發(fā)或者已有的代碼庫(kù)的升級(jí),還是需要支持運(yùn)行時(shí)動(dòng)態(tài)編譯的方式;本部分我們即討論如何編寫(xiě) JSX 格式的 HTML 模板并且進(jìn)行數(shù)據(jù)動(dòng)態(tài)綁定。本部分我們的 HTML 模板即是上文使用的 JSX 代碼,不同的是我們還需要引入 babel-standalone 以及 Ueact 的 umd 模式庫(kù):
然后在本頁(yè)面的 script 標(biāo)簽中,我們可以對(duì)模板進(jìn)行渲染并且綁定數(shù)據(jù):
- var ele = document.querySelector("#inline-jsx");
- Ueact.observeDOM(
- ele,
- {
- state: {
- count: 0,
- delta: 1,
- items: [1, 2, 3]
- },
- methods: {
- handleClick: function () {
- this.state.count+=this.state.delta;
- this.state.items.push(this.state.count);
- },
- handleChange:function (e) {
- let value = parseInt(e.target.value);
- if(!Number.isNaN(value)){
- this.state.delta = value;
- }
- }
- },
- hooks: {
- mounted: function () {
- console.log('mounted');
- },
- updated:function () {
- console.log('updated');
- }
- }
- },
- Babel
- );
這里我們調(diào)用 Ueact.observeDOM 函數(shù)對(duì)模板進(jìn)行渲染,該函數(shù)會(huì)獲取指定元素的 outerHTML 屬性,然后通過(guò) Babel 動(dòng)態(tài)插件進(jìn)行編譯:
- let input = html2JSX(ele.outerHTML);
- let output = Babel.transform(input, {
- presets: ['es2015'],
- plugins: [
- [
- 'transform-react-jsx',
- {
- pragma: 'Ueact.createElement'
- }
- ]
- ]
- }).code;
值得一提的是,因?yàn)?HTML 語(yǔ)法與 JSX 語(yǔ)法存在一定的差異,我們獲取渲染之后的 DOM 對(duì)象之后,還需要對(duì)部分元素語(yǔ)法進(jìn)行修正;主要包括了以下三個(gè)場(chǎng)景:
- 自閉合標(biāo)簽處理,即 =>
- 去除輸入的 HTML 中的事件監(jiān)聽(tīng)的引號(hào),即 onclick="{methods.handleClick}" => onclick={methods.handleClick}
- 移除 value 值額外的引號(hào),即 value="{state.a}" => value={state.a}
到這里我們得到了經(jīng)過(guò) Babel 轉(zhuǎn)化的函數(shù)調(diào)用代碼,下面我們就需要去執(zhí)行這部分代碼并且完成數(shù)據(jù)填充。最簡(jiǎn)單的方式就是使用 eval 函數(shù),不過(guò)因?yàn)樵摵瘮?shù)直接暴露在了全局作用域下,因此并不被建議使用;我們使用動(dòng)態(tài)構(gòu)造 Function 的方式來(lái)進(jìn)行調(diào)用:
- /**
- * Description 從輸入的 JSX 函數(shù)字符串中完成構(gòu)建
- * @param innerContext
- */
- function renderFromStr(innerContext) {
- let func = new Function(
- 'innerContext',
- `
- let { state, methods, hooks } = innerContext;
- let ele = ${innerContext.rawJSX}
- return ele;
- `
- ).bind(innerContext);
- // 構(gòu)建新節(jié)點(diǎn)
- let newEle: Element = func(innerContext);
- // 使用指定元素的父節(jié)點(diǎn)替換自身
- innerContext.root.parentNode.replaceChild(newEle, innerContext.root);
- // 替換完畢之后刪除舊節(jié)點(diǎn)的引用,觸發(fā) GC
- innerContext.root = newEle;
- }
innerContext 即包含了我們定義的 State 與 Methods 等對(duì)象,這里利用 JavaScript 詞法作用域(Lexical Scope)的特性進(jìn)行變量傳遞;本部分完整的代碼參考這里。
變化監(jiān)聽(tīng)與重渲染
筆者在 2015-我的前端之路:數(shù)據(jù)流驅(qū)動(dòng)的界面中討論了從以 DOM 為核心到數(shù)據(jù)流驅(qū)動(dòng)的變化,本部分我們即討論如何自動(dòng)監(jiān)聽(tīng)狀態(tài)變化并且完成重渲染。這里我們采用監(jiān)聽(tīng) JavaScript 對(duì)象屬性的方式進(jìn)行狀態(tài)變化監(jiān)聽(tīng),采用了筆者另一個(gè)庫(kù) Observer-X,其基本用發(fā)如下:
- import { observe } from '../../dist/observer-x';
- const obj = observe(
- {},
- {
- recursive: true
- }
- );
- obj.property = {};
- obj.property.listen(changes => {
- console.log(changes);
- console.log('changes in obj');
- });
- obj.property.name = 1;
- obj.property.arr = [];
- obj.property.arr.listen(changes => {
- // console.log('changes in obj.arr');
- });
- // changes in the single event loop will be print out
- setTimeout(() => {
- obj.property.arr.push(1);
- obj.property.arr.push(2);
- obj.property.arr.splice(0, 0, 3);
- }, 500);
核心即是當(dāng)某個(gè)對(duì)象的屬性發(fā)生變化(增刪賦值)時(shí),觸發(fā)注冊(cè)的回調(diào)事件;即:
- ...
- // 將內(nèi)部狀態(tài)轉(zhuǎn)化為可觀測(cè)變量
- let state = observe(innerContext.state);
- ...
- state.listen(changes => {
- renderFromStr(innerContext);
- innerContext.hooks.updated && innerContext.hooks.updated();
- });
- ...
【本文是專(zhuān)欄作者“張梓雄 ”的原創(chuàng)文章,如需轉(zhuǎn)載請(qǐng)通過(guò)與作者聯(lián)系】
文章題目:基于JSX的動(dòng)態(tài)數(shù)據(jù)綁定
文章位置:http://m.fisionsoft.com.cn/article/djiichs.html


咨詢
建站咨詢
