zoukankan      html  css  js  c++  java
  • 将HTML字符串编译为虚拟DOM对象的基础实现

    本文所有代码均保存在HouyunCheng / mini-2vdom

    虚拟DOM只是实现MVVM的一种方案,或者说是视图更新的一种策略,是实现最小化更新的diff算法的操作对象。

    创建扫描器

    所有编译行为的第一步都是遍历整个字符串,于是我们创建Scanner类,专门用于扫描整个字符串。

    class Scanner {
      constructor(text) {
        this.text  = text;
        // 指针
        this.pos = 0;
        // 尾巴  剩余字符
        this.tail = text;
      }
    
      /**
       * 路过指定内容
       *
       * @memberof Scanner
       */
      scan(tag) {
        if (this.tail.indexOf(tag) === 0) {
          // 直接跳过指定内容的长度
          this.pos += tag.length;
          // 更新tail
          this.tail = this.text.substring(this.pos);
        }
      }
    
      /**
       * 让指针进行扫描,直到遇见指定内容,返回路过的文字
       *
       * @memberof Scanner
       * @return str 收集到的字符串
       */
       scanUntil(stopTag) {
        // 记录开始扫描时的初始值
        const startPos = this.pos;
        // 当尾巴的开头不是stopTg的时候,说明还没有扫描到stopTag
        while (!this.eos() && this.tail.indexOf(stopTag) !== 0 ) {
          // 改变尾巴为当前指针这个字符到最后的所有字符
          this.tail = this.text.substring(++this.pos);
        }
    
        // 返回经过的文本数据
        return this.text.substring(startPos, this.pos).trim();
      }
    
      /**
       * 判断指针是否到达文本末尾(end of string)
       *
       * @memberof Scanner
       */
      eos() {
        return this.pos >= this.text.length;
      }
    }
    

    scanUntil方法用于扫描字符串,并将扫描过的内容返回,用于收集为token。整个扫描会分段进行,直到字符串的结尾。

    转换为没有嵌套结构的tokens

    先看代码,我们先实例化Scanner用于扫描整个传入字符串,同时初始化一个tokens数组用于保存token和一个word用于保存sanner收集到的字符串。

    整个转化行为会持续到字符串的末尾,而scanscanUntil交替进行,不断获取<>之间的内容(即标签和属性)或者><之间的内容(即标签内的内容,包括文本和子标签)。

    为了区分开始标签和结束标签,我们在生成的token数组中的第一项添加#/作为开始或结束的标记,第二项为标签名,第三项,我们放入开始标签中收集到的属性,而不是将属性单独放在一个token中,这样做是为了简化后边将tokens转化为嵌套结构的操作。

    于是,我们得到了由形如[类型标记, 标签名, 数据, 文本]组成的二维数组。

    这里对是一个标签否有属性这一点使用了非常简单粗暴的实现,即看<>中收集到的字符串中是否有空格,有空格则判断为有属性,没空格则判断为没有属性。

    在收集标签属性的时候,顺便使用propsParser对标签属性进行了简单解析。

    /**
     * 将html字符串转为无嵌套结构的token,返回tokens数组
     *
     * @param {string} html
     * @return {array} 
     */
    function collectTokens(html) {
      const scanner = new Scanner(html);
      const tokens = [];
    
      let word = '';
      while (!scanner.eos()) {
        // 扫描文本
        const text = scanner.scanUntil('<');
        scanner.scan('<');
        tokens[tokens.length - 1] && tokens[tokens.length - 1].push(text);
        // 扫描标签<>中的内容
        word = scanner.scanUntil('>');
        scanner.scan('>');
        // 如果没有扫描到值,就跳过本次进行下一次扫描
        if (!word) continue;
        // 区分开始标签 # 和结束标签 /
        if (word.startsWith('/')) {
          tokens.push(['/', word.slice(1)]);
        } else {
          // 如果有属性存在,则解析属性
          const firstSpaceIdx = word.indexOf(' ');
          if (firstSpaceIdx === -1) {
            tokens.push(['#', word, {}]);
          } else {
            // 解析属性
            const data = propsParser(word.slice(firstSpaceIdx))
            tokens.push(['#', word.slice(0, firstSpaceIdx), data]);
          }
        }
      }
    
      return tokens;
    }
    

    使用propsParser简单解析标签属性

    propsParser中,我们同样使用Scanner进行扫描,用=进行分割,分别得到keyvalue

    由于某些属性是单属性的,比如字符串<button loading disabled class="btn">中的loading,以=分割的话会得到loading disabled class作为key,这显然是错误的。于是我们同样使用简单粗暴的方式,用是否有空格来判断是否有单属性,同时将单属性的值设置为true

    由于这里直接使用了"="进行扫描,所以当前的程序不支持单引号,同时="之间不能有空格。

    同时,这里只是对标签属性进行了简单的拆分,并没有对classstyle内的属性进行拆分。那是之后的步骤。当然,也可以放在这里进行。

    function propsParser(propsStr) {
      propsStr = propsStr.trim();
      const scanner = new Scanner(propsStr);
      const props = {};
    
      while(!scanner.eos()) {
        let key = scanner.scanUntil('=');
    
        // 对单属性的处理
        const spaceIdx = key.indexOf(' ');
        if (spaceIdx !== -1) {
          const keys = key.replace(/s+/g, ' ').split(' ');
    
          const len = keys.length;
          for (let i = 0; i < len - 1; i++) {
            props[keys[i]] = true;
          }
          key = keys[len - 1].trim();
        }
        scanner.scan('="');
    
        const val = scanner.scanUntil('"');
        props[key] = val || true;
        scanner.scan('"');
      }
    
      return props;
    }
    

    生成有嵌套结构的tokens

    在之前生成的tokens是没有嵌套结构的,是一个简单的二维数组。在这里,我们要将其转换有嵌套结构的tokens

    对于嵌套结构,通常使用来生成,遇到开始标签(这里为#)则压栈,遇到结束标签(这里为/)则出栈。

    在这里,我们使用stack来保存栈状态,用collector来收集嵌套的内容,在压栈和出栈的同时也修改collector的指向,以保证嵌套层次的准确性。

    同时,我们将嵌套结构放在token的第三个元素的位置。得到形如[类型标记, 标签名, 子节点, 数据, 文本]tokens

    function nestTokens(tokens) {
      const nestedTokens = [];
      const stack = [];
      let collector = nestedTokens;
    
      for (let i = 0, len = tokens.length; i < len; i++) {
        const token = tokens[i];
    
        switch (token[0]) {
          case '#':
            // 收集当前token
            collector.push(token);
            // 压入栈中
            stack.push(token);
            // 由于进入了新的嵌套结构,新建一个数组保存嵌套结构
            // 并修改collector的指向
              token.splice(2, 0, []);
              collector = token[2];
            break;
          case '/':
            // 出栈
            stack.pop();
            // 将收集器指向上一层作用域中用于存放嵌套结构的数组
            collector = stack.length > 0
              ? stack[stack.length - 1][2]
              : nestedTokens;
            break;
          default:
            collector.push(token);
        }
      }
    
      return nestedTokens;
    }
    

    整合tokenizer函数

    有了以上两个函数函数之后,我们可以将其整合为一个函数,方便之后调用。

    function tokenizer(html) {
      return nestTokens(collectTokens(html));
    }
    

    将tokens转换为虚拟DOM

    这一步相对来说就简单很多,只需要安装tokens的结构把相应的数据取出即可。

    同时,在这里我们对classstyle属性进行解析,将形如{class: "item active"}class属性转换为

    {
        class: {
            item: true,
            active: true
        }
    }
    

    的形式。

    将形如{style: "border: 1px solid red; height: 300px"}转换为

    {
        style: {
            border: "border: 1px solid red",
            height: "300px"
        }
    }
    

    的形式。

    同时将在data中的属性key提取出来。由于当前的虚拟DOM还没有上树,所有elm属性为undefined。对于子节点,我们使用递归将子节点追加到children数组中。

    于是最终我们得到形如

    {
        sel: "div",
        children: [{
            sel: "p",
                data: {},
                elm: undefined,
                text: "文本",
                key: "1",
            }
        }],
        data: {class: {container: true}, id: "main"},
        elm: undefined,
        text: undefined,
        key: undefined,
    }
    

    的虚拟DOM结构。

    以下是tokens2vdom的代码实现。

    function tokens2vdom(tokens) {
      const vdom = {};
    
      for (let i = 0, len = tokens.length; i < len; i++) {
        const token = tokens[i];
        vdom['sel'] = token[1];
        vdom['data'] = token[3];
    
        // 解析类名
        if (vdom['data']['class']) {
          vdom['data']['class'] = classParser(vdom['data']['class']);
        }
    
        // 解析行类样式
        if (vdom['data']['style']) {
          vdom['data']['style'] = styleParser(vdom['data']['style']);
        }
    
        // 添加key
        if (vdom['data']['key']) {
          vdom['key'] = vdom['data']['key'];
          delete vdom['data']['key'];
        } else  {
          vdom['key'] = undefined;
        }
    
        if (token[4]) {
          vdom['text'] = token[token.length - 1];
        } else {
          vdom['text'] = undefined;
        }
    
        vdom['elm'] = undefined;
        
        const children = token[2];
        if (children.length === 0) {
          vdom['children'] = undefined;
          continue;
        };
    
        vdom['children'] = [];
    
        for (let j = 0; j < children.length; j++) {
          vdom['children'].push(tokens2vdom([children[j]]));
        }
    
        if (vdom['children'].length === 0) {
          delete vdom['children'];
        }
      }
    
      return vdom;
    }
    

    整合toVDOM函数

    到这里我们的需求就基本实现了,我们将之前的函数整合为一个函数即可。

    function toVDOM (html) {
    
      const tokens = tokenizer(html);
      const vdom = tokens2vdom(tokens);
    
      return vdom;
    }
    

    虚拟DOM的结构参照 snabbdom/snabbdom

    本文完整的代码实现可以查看 HouyunCheng / mini-2vdom

  • 相关阅读:
    selenium+phantomjs报错:Unable to find a free port的分析和解决
    hadoop集群搭建
    虚拟机安装CentOS7 Minimal、jdk和hadoop
    Javascript学习笔记-一些关键点
    隐藏 Win10 中的3D对象、文档、音乐、图片、视频、下载、桌面7个文件夹
    白话网页的网络性能
    (转)“拿人钱财,与人消灾”,这才是员工含义的本质
    JS 小工具 MYSQL WHERE IN条件 去掉换行符(列转行)
    PHP 基于redis的分布式锁
    PHP 将json的int类型转换为string类型 解决php bigint转科学计数法的问题
  • 原文地址:https://www.cnblogs.com/hycstar/p/14744413.html
Copyright © 2011-2022 走看看