资讯专栏INFORMATION COLUMN

简单实现迷你Vue框架

soasme / 2478人阅读

摘要:它们不是主树的一部分。在树中,文档片段被其所有的子元素所代替。因为文档片段存在于内存中,并不在树中,所以将子元素插入到文档片段时不会引起页面回流对元素位置和几何上的计算。因此,使用文档片段通常会带来更好的性能。

本教程说明将采用es6语法来编写

创建MiniVue.js文件

//创建一个MVVM类
class MVVM {
  // 构造器
  constructor(option) {
    // 缓存重要属性
    this.$vm = this;
    this.$el = option.el;
    this.$data = option.data;
  }
}

MVVM类的作用: 解析视图模板,把对应的数据,渲染到视图

首先得判断视图是否存在,在视图存在的时候,创建模板编译器,来解析视图

class MVVM {
  // 构造器
  constructor(option) {
    // 缓存重要属性
    this.$vm = this;
    this.$el = option.el;
    this.$data = option.data;
    // 判断视图是否存在
    if (this.$el) {
      // 创建模板编译器, 来解析视图
      this.$compiler = new TemplateCompiler(this.$el, this.$vm)
    }
  }
}
下面我们来创建文件TemplateCompiler.js, 输入以下内容
// 创建一个模板编译工具
class TemplateCompiler {
  // el 视图
  // vm 全局vm对象
  constructor(el, vm) {
    // 缓存重要属性
    this.el = document.querySelector(el);
    this.vm = vm;
  }
} 

当缓存好重要的属性后,就要解析模板了

步骤分三步

把模板内容放进内存(内存片段)

解析模板

把内存的结果,放回到模板

class TemplateCompiler {

// el 视图
// vm 全局vm对象
constructor(el, vm) {
  // 缓存重要属性
  this.el = document.querySelector(el);
  this.vm = vm;
  
  // 1. 把模板内容放进内存(内存片段)
  let fragment = this.node2fragment(this.el);
  // 2. 解析模板
  this.compile(fragment);
  // 3. 把内存的结果,放回到模板
  this.el.appendChild(fragment);
}

}

上面定义node2fragment()方法和compile()方法下面我们来实现

分析TemplateCompiler 类有两大类方法:工具方法、核心方法
    class TemplateCompiler {
      // el 视图
      // vm 全局vm对象
      constructor(el, vm) {
        // 缓存重要属性
        this.el = document.querySelector(el);
        this.vm = vm;
    
        // 1. 把模板内容放进内存(内存片段)
        let fragment = this.node2fragment(this.el)
        // 2. 解析模板
        this.compile(fragment);
        // 3. 把内存的结果,放回到模板
        this.el.appendChild(fragment);
       }
      }

  // 工具方法
  isElementNode(node) {
    // 1. 元素节点  2. 属性节点  3. 文本节点
    return node.nodeType === 1;
  }
  isTextNode(node) {
    return node.nodeType === 3;
  }

  // 核心方法
  node2fragment(node) {
    // 1. 创建内存片段
    let fragment = document.createDocumentFragment();
    // 2. 把模板内容放进内存
    let child;
    while (child = node.firstChild) {
      fragment.appendChild(child);
    }
    // 3. 返回
    return fragment;
  }
  compile(node) {
  }
}
关于createDocumentFragment的描述:

DocumentFragments 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的子元素所代替。

因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。

分析解析模板compile()方法实现方式

首先获取每一个子节点,然后遍历每一个节点,再判断节点类型
下面childNode是类数组没有数组方法

使用扩展运算符( spread )[...childNode]使其转化为数组便于操作

compile(parent) {
    // 1. 获取子节点
    let childNode = parent.childNodes;
    // 2. 遍历每一个节点
    [...childNode].forEach(node => {
      // 3. 判断节点类型
      if (this.isElementNode(node)) {
        // 解析元素节点的指令
        this.compileElement(node);
      }
    })
  }

下面来实现compileElement()方法 当前调用传过来的是元素节点

接下来就要获取元素的所有属性,然后遍历所有属性,再判断属性是否是指令

v-text是vue的指令,像ng-xxx指令是不进行收集的
判断为指令时就要收集结果
compileElement(node) {
    // 1. 获取当前节点的所有属性
    let attrs = node.attributes;
    // 2. 遍历当前元素的所有属性
    [...attrs].forEach(attr => {
      let attrName = attr.name
      // 3. 判断属性是否是指令
      if (this.isDirective(attrName)) {
        // 4. 收集
        let type = attrName.substr(2); // v-text
        // 指令的值就是表达式
        let expr = attr.value;
        // 解析指令
        CompilerUtils.text(node, this.vm, expr);
      }
    })
  }
实现CompilerUtils类
CompilerUtils = {
  // 解析text指令
  text(node, vm, expr) {
    // 1. 找到更新方法
    let updaterFn = this.updater["textUpdater"];
    // 执行方法
    updaterFn && updaterFn(node, vm.$data[expr]);
  },
  // 更新规则对象
  updater: {
    // 文本更新方法
    textUpdater(node, value) {
      node.textContent = value;
    }
  }
}
在TemplateCompiler类工具方法下添加方法isDirective()判断属性是否是指令
isDirective(attrName) {
    // 判断属性是否是指令
    return attrName.indexOf("v-") >= 0;
  }
实现(v-text)完整代码
// 创建一个模板编译工具
class TemplateCompiler {
  // el 视图
  // vm 全局vm对象
  constructor(el, vm) {
    // 缓存重要属性
    this.el = document.querySelector(el);
    this.vm = vm;
    // 1. 把模板内容放进内存(内存片段)
    let fragment = this.node2fragment(this.el)
    // 2. 解析模板
    this.compile(fragment);
    // 3. 把内存的结果,放回到模板
    this.el.appendChild(fragment);
  }
  // 工具方法
  isElementNode(node) {
    // 1. 元素节点  2. 属性节点  3. 文本节点
    return node.nodeType === 1;
  }
  isTextNode(node) {
    return node.nodeType === 3;
  }
  isDirective(attrName) {
    // 判断属性是否是指令
    return attrName.indexOf("v-") >= 0;
  }
  // 核心方法
  node2fragment(node) {
    // 1. 创建内存片段
    let fragment = document.createDocumentFragment();
    // 2. 把模板内容放进内存
    let child;
    while (child = node.firstChild) {
      fragment.appendChild(child);
    }
    // 3. 返回
    return fragment;
  }
  compile(parent) {
    // 1. 获取子节点
    let childNode = parent.childNodes;
    // 2. 遍历每一个节点
    [...childNode].forEach(node => {
      // 3. 判断节点类型
      if (this.isElementNode(node)) {
        // 元素节点 (解析指令)
        this.compileElement(node);
      }
    })
  }
  // 解析元素节点的指令
  compileElement(node) {
    // 1. 获取当前节点的所有属性
    let attrs = node.attributes;
    // 2. 遍历当前元素的所有属性
    [...attrs].forEach(attr => {
      let attrName = attr.name
      // 3. 判断属性是否是指令
      if (this.isDirective(attrName)) {
        // 4. 收集
        let type = attrName.substr(2); // v-text
        // 指令的值就是表达式
        let expr = attr.value;
        CompilerUtils.text(node, this.vm, expr);
      }
    })
  }
  // 解析表达式
  compileText() {
  }
}
CompilerUtils = {
  // 解析text指令
  text(node, vm, expr) {
    // 1. 找到更新方法
    let updaterFn = this.updater["textUpdater"];
    // 执行方法
    updaterFn && updaterFn(node, vm.$data[expr]);
  },
  // 更新规则对象
  updater: {
    // 文本更新方法
    textUpdater(node, value) {
      node.textContent = value;
    }
  }
}
下面来实现(v-model)

修改如下代码

compileElement(node) {
  // 1. 获取当前节点的所有属性
  let attrs = node.attributes;
  // 2. 遍历当前元素的所有属性
  [...attrs].forEach(attr => {
    let attrName = attr.name
    // 3. 判断属性是否是指令
    if (this.isDirective(attrName)) {
      // 4. 收集
      let type = attrName.substr(2); // v-text
      // 指令的值就是表达式
      let expr = attr.value;
      //-----------------------修改代码start---------------------
      // CompilerUtils.text(node, this.vm, expr);
      CompilerUtils[type](node, this.vm, expr);
      //-----------------------修改代码end---------------------
    }
  })
}

在CompilerUtils类添加实现

CompilerUtils = {
  ...
  //----------------------新增方法---------------------
  // 解析model指令
  model(node, vm, expr) {
    // 1. 找到更新方法
    let updaterFn = this.updater["modelUpdater"];
    // 执行方法
    updaterFn && updaterFn(node, vm.$data[expr]);
  },
  // 更新规则对象
  updater: {
    ...
    //----------------------新增方法---------------------
    // 输入框更新方法
    modelUpdater(node, value) {
      node.value = value
    }
  }
}

实现(v-html)就由读者自行添加对应的方法,形式和(v-model)差不多

下面来实现解析表达式 ,在compile添加以下代码

重要的怎么用验证表达式

compile(parent) {
    // 1. 获取子节点
    let childNode = parent.childNodes;
    // 2. 遍历每一个节点
    [...childNode].forEach(node => {
      // 3. 判断节点类型
      if (this.isElementNode(node)) {
        // 元素节点 (解析指令)
        this.compileElement(node);
      //-----------------新增代码--------------------
        // 文本节点
      } else if (this.isTextNode(node)) {
        // 表达式解析
        // 定义表达式正则验证规则
        let textReg = /{{(.+)}}/;
        let expr = node.textContent;
        // 按照规则验证内容
        if (textReg.test(expr)) {
          // 获取分组内容
          expr = RegExp.$1;
          // 调用方法编译
          this.compileText(node, expr);
        }
      }
    })
  }

下面来实现文本解析器,通过分析(v-text)和表达式解析差不多

// 解析表达式
  compileText(node, expr) {
    CompilerUtils.text(node, this.vm, expr);
  }

完整实现代码

// 创建一个模板编译工具
class TemplateCompiler {
  // el 视图
  // vm 全局vm对象
  constructor(el, vm) {
    // 缓存重要属性
    this.el = document.querySelector(el);
    this.vm = vm;
    // 1. 把模板内容放进内存(内存片段)
    let fragment = this.node2fragment(this.el)
    // 2. 解析模板
    this.compile(fragment);
    // 3. 把内存的结果,放回到模板
    this.el.appendChild(fragment);
  }
  // 工具方法
  isElementNode(node) {
    // 1. 元素节点  2. 属性节点  3. 文本节点
    return node.nodeType === 1;
  }
  isTextNode(node) {
    return node.nodeType === 3;
  }
  isDirective(attrName) {
    // 判断属性是否是指令
    return attrName.indexOf("v-") >= 0;
  }
  // 核心方法
  node2fragment(node) {
    // 1. 创建内存片段
    let fragment = document.createDocumentFragment();
    // 2. 把模板内容放进内存
    let child;
    while (child = node.firstChild) {
      fragment.appendChild(child);
    }
    // 3. 返回
    return fragment;
  }
  compile(parent) {
    // 1. 获取子节点
    let childNode = parent.childNodes;
    // 2. 遍历每一个节点
    [...childNode].forEach(node => {
      // 3. 判断节点类型
      if (this.isElementNode(node)) {
        // 元素节点 (解析指令)
        this.compileElement(node);
      } else if (this.isTextNode(node)) {
        // 表达式解析
        // 定义表达式正则验证规则
        let textReg = /{{(.+)}}/;
        let expr = node.textContent;
        // 按照规则验证内容
        if (textReg.test(expr)) {
          expr = RegExp.$1;
          // 调用方法编译
          this.compileText(node, expr);
        }
      }
    })
  }
  // 解析元素节点的指令
  compileElement(node) {
    // 1. 获取当前节点的所有属性
    let attrs = node.attributes;
    // 2. 遍历当前元素的所有属性
    [...attrs].forEach(attr => {
      let attrName = attr.name;
      // 3. 判断属性是否是指令
      if (this.isDirective(attrName)) {
        // 4. 收集
        let type = attrName.substr(2); // v-text
        // 指令的值就是表达式
        let expr = attr.value;
        // CompilerUtils.text(node, this.vm, expr);
        CompilerUtils[type](node, this.vm, expr);
      }
    })
  }
  // 解析表达式
  compileText(node, expr) {
    CompilerUtils.text(node, this.vm, expr);
  }
}
CompilerUtils = {
  // 解析text指令
  text(node, vm, expr) {
    // 1. 找到更新方法
    let updaterFn = this.updater["textUpdater"];
    // 执行方法
    updaterFn && updaterFn(node, vm.$data[expr]);
  },
  // 解析model指令
  model(node, vm, expr) {
    // 1. 找到更新方法
    let updaterFn = this.updater["modelUpdater"];
    // 执行方法
    updaterFn && updaterFn(node, vm.$data[expr]);
  },
  // 更新规则对象
  updater: {
    // 文本更新方法
    textUpdater(node, value) {
      node.textContent = value;
    },
    // 输入框更新方法
    modelUpdater(node, value) {
      node.value = value;
    }
  }
}

数据双向绑定(完结篇)

后续内容更精彩

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/100953.html

相关文章

  • 迷你Vue--学习如何造一个Vue轮子

    摘要:项目地址和的区别其实和最大的区别就是多了一个虚拟,其他的区别都是很小的。 项目地址 Vue1和Vue2的区别 其实Vue1和Vue2最大的区别就是Vue2多了一个虚拟DOM,其他的区别都是很小的。所以理解了Vue1的源码,就相当于理解了Vue2,中间差了一个虚拟DOM的Diff算法 文档 数据双向绑定 Vue主流程走向 组件 nextTick异步更新 MVVM 先来科普一下MVVM...

    isLishude 评论0 收藏0
  • 去哪儿网迷你React的研发心得

    摘要:市面上竟然拥有多个虚拟库。虚拟库,就是出来后的一种新式库,以虚拟与算法为核心,屏蔽操作,操作数据即操作视图。及其他虚拟库已经将虚拟的生成交由与处理了,因此不同点是,虚拟的结构与算法。因此虚拟库是分为两大派系算法派与拟态派。 去哪儿网迷你React是年初立项的新作品,在这前,去哪儿网已经深耕多年,拥有QRN(react-native的公司制定版),HY(基于React的hybird方案)...

    pekonchan 评论0 收藏0
  • 吐槽专用

    摘要:最终选择了兼容到的,终于使用上框架,虽然它只是个。没有对比就没有伤害本来想着技术栈统一,移动端也准备使用。于是,之后对移动端的技术选型上更加慎重了,最终采用了文档更漂亮的。易用还真不易用,坑还真多。 吐槽 avalon.js 历史背景 需求重大调整,所有业务推倒重来(pc端主要任务涉及管理后台类型的网站); 开发周期很紧,过年前要上线; 公司pc端业务要求兼容到ie8; 2015年前...

    zxhaaa 评论0 收藏0
  • Fre:又一个小而美的前端MVVM框架

    摘要:,大家好,好久不贱呢最近因为看了一些的小说,整个人都比较致郁就在昨天,我用了一天的时间写了,又一个小而美的前端框架可能你觉得,有了和,没必要再写一个了我觉得我还是想想办法寻找一下它的存在感吧先看的组件化方案最先看到的应该是。 halo,大家好,好久不贱呢! 最近因为看了一些 be 的小说,整个人都比较致郁::>__+ {state.count--}}>- ...

    LittleLiByte 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<