logo学习随笔

【译】神奇的模板字符串

December 01, 2019

背景

最近在个人博客中采用了styled-components库来写组件的样式,在翻看文档时,发现用法:

import styled from 'styled-components';

const Title = styled.h1`
  color: palevioletred;
  font-size: 18px;
`;

const App = () => <Title>Hello World!</Title>;

这个用法引起了我的兴趣,这是如何 work 起来的?

带标签的模板字符串(Tagged Template Literals)

styled.h1 符号其实是模板字符串的一种高级用法,本质上它只是调用了一个函数即 styled.h1``\styled.h1() 功能是同样的。不过差异在于两种写法的传参方式。

我们来看下面简单的例子:

const logArgs = (...args) => console.log(args);

logArgs('a', 'b');
// -> a b

作为标签模板字符串来调用:

logArgs``;
// -> ['']

logArgs`I like pizza`;
// -> ['I like pizza']

可以看出,模板字符串方式会把传入的参数以数组来包一层。

插值(Interpolations)

模板字符串中可以存在插值,如:

const favoriteFood = 'pizza';

logArgs(`I like ${favoriteFood}.`);
// -> I like pizza.

可以看到,JavaScript 执行时会把变量插入到传入函数参数的字符串中。那在带有标签的模板字符串中使用时有什么表现:

const favoriteFood = 'pizza';

logArgs`I like ${favoriteFood}.`;
// -> ['I like ', '.'] 'pizza'

我们从结果中看到第一个结果还是字符串的数组,里面的元素在插值处分割而插值的内容则作为第二个参数被传递。

logargs explanation

纯文本的字符串都被存放在第一个参数数组内,随后为插值内容。

当多于一个插值时:

const favoriteFood = 'pizza';
const favoriteDrink = 'cola';

logArgs`I like ${favoriteFood} and ${favoriteDrink}.`;
// -> ['I like ', ' and ', '.'] 'pizza' 'cola'

每个插值都成为函数调用的下一个参数,我们可以有任意多个插值参数。

与正常的函数调用相比:

const favoriteFood = 'pizza';
const favoriteDrink = 'cola';

logArgs(`I like ${favoriteFood} and ${favoriteDrink}.`);
// -> I like pizza and cola.

所有的插值都混为了一个大字符串。

优势

这种方式开启了新的探索。以 styled-components 为例,我们有一个 <Button /> 组件,当 props 传入 primary 时期望按钮看起来更大,<Button primary />。 我们可以如下来实现:

const Button = styled.button`
  font-size: ${props => (props.primary ? '2em' : '1em')};
`;

得到的结果:

// font-size: 2em;
<Button primary />

// font-size: 1em;
<Button />

让我们再回过来看 logArgs 函数。

logArgs(`Test ${() => console.log('test')}`);
// -> Test () => console.log('test');

这里注意得到的结果为整个字符串,而没有真正的函数。

当以带标签的模板字符串调用时:

logArgs`Test ${() => console.log('test')}`;
// ['Test', ''] () => console.log('test')

从结果上可能看不出,但这里得到了真正的函数(而不是字符串)。

这意味着我们可以得到这个函数并且执行它。我们来看一个新的可以执行函数类型参数的函数:

const execFuncArgs = (...args) =>
  args.forEach(arg => {
    if (typeof arg === 'function') {
      arg();
    }
  });

这个函数执行时会执行类型为函数的参数:

execFuncArgs('a', 'b');
// -> undefined

execFuncArgs(() => {
  console.log('this is a function');
});
// -> this is a function

execFuncArgs('a', () => {
  console.log('another one');
});
// -> another one

我们再来看:

execFuncArgs(
  `Hi, ${() => {
    console.log('Executed!');
  }}`
);
// -> undefined

execFuncArgs`Hi, ${() => console.log('Executed!')}`;
// -> 'Executed!'

在带标签的模板字符串中 execFuncArgs 的第二个参数传递的是实际函数,然后继续执行该函数。styled-components 底层的实现就是依赖这个特性,在渲染时传入插值函数的 props 可以帮用户来动态更新样式。

总结

可以看到,JavaScript 的一个新特性就可以实现一个强大的库,为我们的开发带来更好的体验,我们需要不断去探索去总结去实践 JavaScript 实现更多新的能力。

参考

https://mxstbr.blog/2016/11/styled-components-magic-explained/