Skip to content

Commit

Permalink
feat: implement v-once
Browse files Browse the repository at this point in the history
  • Loading branch information
phoenix-ru committed Nov 22, 2023
1 parent b0b7466 commit 2cbfad7
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 7 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ Code generator
- [x] v-cloak
- [x] v-html
- [ ] v-memo
- [ ] v-once
- [x] v-once
- [x] v-pre
- [x] v-text
- [x] Custom directives
Expand Down
7 changes: 6 additions & 1 deletion crates/fervid_codegen/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ use fervid_core::BindingsHelper;
#[derive(Debug, Default)]
pub struct CodegenContext {
pub bindings_helper: BindingsHelper,
pub is_cache_disabled: bool,
pub next_cache_index: u8
}

impl CodegenContext {
pub fn with_bindings_helper(bindings_helper: BindingsHelper) -> CodegenContext {
CodegenContext { bindings_helper }
CodegenContext {
bindings_helper,
..Default::default()
}
}
}
33 changes: 32 additions & 1 deletion crates/fervid_codegen/src/control_flow/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,26 @@ impl CodegenContext {
}
}

/// Generates the HTML element, component or a Vue built-in.
pub fn generate_element_or_component(
&mut self,
element_node: &ElementNode,
wrap_in_block: bool,
) -> Expr {
// `v-once` logic is common for all
let has_v_once = element_node
.starting_tag
.directives
.as_ref()
.and_then(|directives| directives.v_once)
.is_some();

// Disable caching if `v-once` is present
let old_is_cache_disabled = self.is_cache_disabled;
if has_v_once {
self.is_cache_disabled = true;
}

let mut result = match element_node.kind {
ElementKind::Builtin(builtin_type) => self.generate_builtin(element_node, builtin_type),

Expand All @@ -45,10 +60,18 @@ impl CodegenContext {
// Generate `v-for` if it is present
if let Some(ref directives) = element_node.starting_tag.directives {
if let Some(ref v_for) = directives.v_for {
result = self.generate_v_for(v_for, result);
result = self.generate_v_for(v_for, Box::new(result));
}
}

// Generate `v-once` if needed
if has_v_once {
result = self.generate_v_once(Box::new(result));

// Restore caching
self.is_cache_disabled = old_is_cache_disabled;
}

result
}

Expand Down Expand Up @@ -172,6 +195,14 @@ impl CodegenContext {
}
}

/// Produce the index for a next `cache[idx]` entry.
/// This is useful for a `v-once` or event handlers.
pub fn allocate_next_cache_entry(&mut self) -> u8 {
let idx = self.next_cache_index;
self.next_cache_index += 1;
idx
}

fn concatenate_text_nodes(
&mut self,
text_nodes_concatenation: &mut TextNodesConcatenationVec,
Expand Down
1 change: 1 addition & 0 deletions crates/fervid_codegen/src/directives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::{utils::str_to_propname, CodegenContext};
mod v_for;
mod v_html;
mod v_model;
mod v_once;
mod v_text;

impl CodegenContext {
Expand Down
6 changes: 2 additions & 4 deletions crates/fervid_codegen/src/directives/v_for.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,14 @@ use crate::CodegenContext;

impl CodegenContext {
/// Generates `(openBlock(true), createElementBlock(Fragment, null, renderList(<list>, (<item>) => (<expr>)), <patch flag>))`
pub fn generate_v_for(&mut self, v_for: &VForDirective, item_render_expr: Expr) -> Expr {
pub fn generate_v_for(&mut self, v_for: &VForDirective, item_render_expr: Box<Expr>) -> Expr {
let span = DUMMY_SP; // TODO

// Arrow function which renders each individual item
let render_list_arrow = Expr::Arrow(ArrowExpr {
span,
params: vec![Pat::Expr(v_for.itervar.to_owned())],
body: Box::new(swc_core::ecma::ast::BlockStmtOrExpr::Expr(Box::new(
item_render_expr,
))),
body: Box::new(swc_core::ecma::ast::BlockStmtOrExpr::Expr(item_render_expr)),
is_async: false,
is_generator: false,
type_params: None,
Expand Down
135 changes: 135 additions & 0 deletions crates/fervid_codegen/src/directives/v_once.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use fervid_core::{fervid_atom, VueImports};
use swc_core::{
common::DUMMY_SP,
ecma::ast::{CallExpr, Callee, Expr, ExprOrSpread, Ident, Lit, Number, AssignExpr, AssignOp, MemberExpr, ComputedPropName, ParenExpr, SeqExpr, BinExpr, BinaryOp},
};

use crate::CodegenContext;

impl CodegenContext {
/// Generates the complex cache structure for `v-once`.
///
/// ## Example
/// In:
/// `<div v-once></div>`
///
/// Out:
/// ```js
/// _cache[0] || (
/// _setBlockTracking(-1),
/// _cache[0] = _createElementVNode("div"),
/// _setBlockTracking(1),
/// _cache[0]
/// )
/// ```
pub fn generate_v_once(&mut self, item_render_expr: Box<Expr>) -> Expr {
// Prepare
let cache_idx = self.allocate_next_cache_entry();
let cache_ident = fervid_atom!("_cache");
let set_block_tracking_ident = Box::new(Expr::Ident(Ident {
span: DUMMY_SP,
sym: self.get_and_add_import_ident(VueImports::SetBlockTracking),
optional: false,
}));

// `_setBlockTracking($value)`
macro_rules! set_block_tracking {
($value: literal, $ident: expr) => {
Box::new(Expr::Call(CallExpr {
span: DUMMY_SP,
callee: Callee::Expr($ident),
args: vec![ExprOrSpread {
spread: None,
expr: Box::new(Expr::Lit(Lit::Num(Number {
span: DUMMY_SP,
value: $value,
raw: None,
}))),
}],
type_args: None,
}))
};
}

// 1. `_cache[cache_idx]`
let cache_expr = Box::new(Expr::Member(MemberExpr {
span: DUMMY_SP,
obj: Box::new(Expr::Ident(Ident { span: DUMMY_SP, sym: cache_ident, optional: false })),
prop: swc_core::ecma::ast::MemberProp::Computed(ComputedPropName {
span: DUMMY_SP,
expr: Box::new(Expr::Lit(Lit::Num(Number { span: DUMMY_SP, value: cache_idx as f64, raw: None }))),
}),
}));

// 2. `_setBlockTracking(-1)`
let decrement_tracking = set_block_tracking!(-1.0, set_block_tracking_ident.to_owned());

// 3. `_cache[idx] = item_render_expr`
let cache_assign = Box::new(Expr::Assign(AssignExpr {
span: DUMMY_SP,
op: AssignOp::Assign,
left: swc_core::ecma::ast::PatOrExpr::Expr(cache_expr.to_owned()),
right: item_render_expr,
}));

// 4. `_setBlockTracking(1)`
let increment_tracking = set_block_tracking!(1.0, set_block_tracking_ident);

// 5. Combine to
// (
// _setBlockTracking(-1),
// _cache[0] = _createElementVNode("div"),
// _setBlockTracking(1),
// _cache[0]
// )
let parens_expr = Box::new(Expr::Paren(ParenExpr {
span: DUMMY_SP,
expr: Box::new(Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs: vec![
decrement_tracking,
cache_assign,
increment_tracking,
cache_expr.to_owned()
],
})),
}));

// 6. Combine to the final form
Expr::Bin(BinExpr {
span: DUMMY_SP,
op: BinaryOp::LogicalOr,
left: cache_expr,
right: parens_expr,
})
}
}

#[cfg(test)]
mod tests {
use crate::test_utils::js;

use super::*;

#[test]
fn it_generates_v_once() {
let mut ctx = CodegenContext::default();

// Mock a render expression
let item_render_expr = js("_createElementVNode(\"div\")");

// First `v-once`
let v_once_expr = ctx.generate_v_once(item_render_expr.to_owned());
assert_eq!(
crate::test_utils::to_str(v_once_expr),
"_cache[0]||(_setBlockTracking(-1),_cache[0]=_createElementVNode(\"div\"),_setBlockTracking(1),_cache[0])"
);

// Second `v-once` with increased cache index
let v_once_expr = ctx.generate_v_once(item_render_expr);
assert_eq!(
crate::test_utils::to_str(v_once_expr),
"_cache[1]||(_setBlockTracking(-1),_cache[1]=_createElementVNode(\"div\"),_setBlockTracking(1),_cache[1])"
);
}
}
2 changes: 2 additions & 0 deletions crates/fervid_core/src/vue_imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ flags! {
ResolveComponent,
ResolveDirective,
ResolveDynamicComponent,
SetBlockTracking,
Suspense,
Teleport,
ToDisplayString,
Expand Down Expand Up @@ -65,6 +66,7 @@ impl VueImports {
VueImports::ResolveComponent => "_resolveComponent",
VueImports::ResolveDirective => "_resolveDirective",
VueImports::ResolveDynamicComponent => "_resolveDynamicComponent",
VueImports::SetBlockTracking => "_setBlockTracking",
VueImports::Suspense => "_Suspense",
VueImports::Teleport => "_Teleport",
VueImports::ToDisplayString => "_toDisplayString",
Expand Down

0 comments on commit 2cbfad7

Please sign in to comment.