Vue原理之模板编译

// 模板内容
<div id="app">
  <input type="text" v-model="message">
  {{message}}
  <div>{{a.b}}</div>
</div>

// vue脚本
let vm = new Vue({
  el: '#app',
  data: {
    message: '我是message',
    a: {
      b: '我的a.b' 
    }
  }
})
复制代码

看到上面的代码,使用过vue的同学能知道页面的渲染结果会如下图所示:

那他是如何进行渲染的呢,我们带着问题来进入正题。

首先新建Vue.js并创建一个名为Vue的类:

class Vue {
  constructor(options) {
    // 挂载可用数据到实例上
    this.$el = options.el;
    this.$data = options.data;

    // 如果含有模板就去编译
    if (this.$el) {
      // 用数据和元素进行编译
      new Compile(this.$el, this);
    }
  }
}
复制代码

以上代码就是对new Vue时传递的参数el和data进行存储,再利用Compile来对编译模板。

Compile类对模板进行处理:

新建一个compile.js的文件,并创建Compile类

class Compile{
  constructor(el ,vm) {
    this.el = this.isElememtNode(el) ? el : document.querySelector(el);
    this.vm = vm;
    if (this.el) {
      // 把需要操作的dom先放到内存中
      let fragment = this.node2fragment(this.el);
      // 编译:提取元素节点的v-model和文本节点{{}}
      this.compile(fragment);
      // 把编译完成的元素放到页面中
      this.el.appendChild(fragment);
    }
  }
}
复制代码

由于Vue中的el是可以传递选择器和元素节点的,我们这里也对el做了相应的处理。

判断用户传递的el是否是元素节点,如果是元素节点使用,如果是选择器,就获取元素后进行使用。

// isElememtNode
isElememtNode(node) {
  return node.nodeType === 1;
}
复制代码

获取跟元素节点后,利用node2fragment函数把dom元素放入内存中处理:

 node2fragment(el) {
   // 创建文档碎片
   let fragment = document.createDocumentFragment();
   let firstChild;
   while(firstChild = el.firstChild) {
     // 把dom元素移入到fragment
     fragment.appendChild(firstChild);
   }
   return fragment;
 }
复制代码

这样我们就得到了fragment,接下来的处理,我们只需要对fragment进行处理即可。

拿到了文档碎片fragment,我们就可以开始编写Compile核心函数了

compile(fragment) {
  // 获取fragment的所有子元素
  let childNodes = fragment.childNodes;
  Array.from(childNodes).forEach(node => {
  if (this.isElememtNode(node)) {
      // 编译元素
      this.compileElement(node);
      // 递归执行
      this.compile(node);
    } else {
      this.compileText(node);
    }
  })
}
复制代码

获取所有子元素后,分别针对是元素节点和文本节点的情况进行处理,需要指出的一点就是,元素节点内部可能还有子元素, 所以我们以当前子节点为参数递归执行compile。

我们再分别来看一下compileElement和compileText两个方法

compileText

// 编译文本节点
compileText(node) {
  let expr = node.textContent;
  // 匹配开头是{{结尾是}}并且中间不存在}的值
  let reg = /\{\{([^}]+)\}\}/g;
  if (reg.test(expr)) {
    CompileUtil['text'](node, this.vm, expr);
  }
}

复制代码

其中用到的正则:

/\{\{([^}]+)\}\}/g;
复制代码

如果对这个正则不理解,我们可以配合图来理解一下

他实现的功能就是匹配开头是 {{ 结尾是 }} 并且中间不存在 } 的字符串模板。

得到字符串模板之后我们就可以vm实例中取到对应的值,具体的处理,我们分离到CompileUtil中来实现。

compileElement

如果是元素节点,我们需要考虑的就是其存在指令的情况(本篇文章只讲述v-model的情况)

我们分为三步来实现该功能

  1. 获取元素节点的属性集合
  2. 判断属性是否为指令(isDirective函数)
  3. 如果是指令,利用CompileUtil函数做对应处理。
// 编译元素节点
compileElement(node) {
  let attrs = node.attributes; // 获取当前节点的属性
  Array.from(attrs).forEach(attr => {
    let attrName = attr.name;
    // 如果是指令进行数据处理
    if (this.isDirective(attrName)) {
      let expr = attr.value;
      let [,type] = attrName.split('-');
      CompileUtil[type](node, this.vm, expr)
    }
  })
}

// 如果是v-开头,我们就认为他是指令
isDirective(name) {
  return name.startsWith('v-');
}
复制代码

以上compileText和compileElement两个方法中,具体的处理方式都使用到了CompileUtil这个辅助类,我们可以来看一下其代码实现。

CompileUtil

我们先来看对于text的处理。

经过以上的处理,我们会拿到类似于{{XXX}}的字符串,有了这个字符串,我们还需要下面几步:

  1. 得到{{xxx}}中的xxx
  2. 寻找vm.$data中xxx对应的值
  3. 得到对应值后,更新对应节点的文本内容

上面需要处理的一个难点是:我们的需要的值可能是对象中的对象,类似于{{a.b.c}},解决方案为:先把字符串分隔成数组,再使用reduce每次都取到下一个key,最后利用key取到对应对象的值。

// 编译所需的辅助方法
CompileUtil = {
  getVal(vm, expr) { // 获取实例上对应的数据
    expr = expr.split('.');
    return expr.reduce((prev, next) => {
      return prev[next];
    }, vm.$data);
  },
  
  getTextVal(expr, vm) {
    return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
      // expr: {{XXX}}
      // arguments[1]是XXX
      return this.getVal(vm, arguments[1]);
    });
  },

  text(node, vm, expr) { // 文本处理
    let updateFn = this.updater['textUpdater'];
    let value = this.getTextVal(expr, vm);
    updateFn && updateFn(node, value);
  },

  updater: {
    textUpdater(node, value) {
      node.textContent = value;
    }
  }
}
复制代码

处理完了text,再来看如何处理指令

在上面的compileElement方法中,我们判断了节点属性是否是指令,如果是指令我们就拿到具体的指令,例如v-model我们就拿到model,到这里,我们还需要以下几步:

  1. 获取到指令所对应的key,例如v-model=“message"中的message
  2. 更新节点的value值为vm.data对应数据的值,例如vm.data.message
  3. 设置节点的value值为对应的值

为了实现以上需求,我们给CompileUtil新增model方法

model(node, vm ,expr) { // v-model处理
  let updateFn = this.updater['modelUpdater'];
  updateFn && updateFn(node, this.getVal(vm, expr));
},
复制代码

对应的modelUpdater:

modelUpdater(node, value) {
  node.value = value;
}
复制代码

完整的CompileUtil代码如下

// 编译所需的辅助方法
CompileUtil = {
  getVal(vm, expr) { // 获取实例上对应的数据
    expr = expr.split('.');
    return expr.reduce((prev, next) => {
      return prev[next];
    }, vm.$data);
  },
  
  getTextVal(expr, vm) {
    return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
      return this.getVal(vm, arguments[1]);
    });
  },

  text(node, vm, expr) { // 文本处理
    let updateFn = this.updater['textUpdater'];
    let value = this.getTextVal(expr, vm);
    updateFn && updateFn(node, value);
  },

  model(node, vm ,expr) { // v-model处理
    let updateFn = this.updater['modelUpdater'];
    updateFn && updateFn(node, this.getVal(vm, expr));
  },

  updater: {
    textUpdater(node, value) {
      node.textContent = value;
    },
    modelUpdater(node, value) {
      node.value = value;
    }
  }
}
复制代码

到这里,文本节点和v-model指令的编译都已经完成。

最后一步,就是把文档碎片fragment放回到根节点中去

this.el.appendChild(fragment);
复制代码

到这里,一个基础的编译环节就宣告完成,打开页面就能得到期待的渲染结果了???

斗胆发文,欢迎吐槽和指正。

附上完整代码示例,期待与您共同进步:github.com/Ljhhhhhh/mv…

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值