Vue底层学习6——节点编译与属性遍历

全手打原创,转载请标明出处:https://www.cnblogs.com/dreamsqin/p/15175740.html, 多谢,=。=~(如果对你有帮助的话请帮我点个赞啦)

作为一个Web前端开发人员,使用Vue框架进行项目开发已经有一阵子,掐指一算,是时候认真探索一下Vue的底层了,以前的了解比较偏理论,这一次打算在弄清基本原理的前提下自己手写Vue中的核心部分,也许这样我才敢说自己“深入理解”了Vue。上篇完成了编译器的第一项目标:插值文本编译和依赖收集,编译后template中的插值变量会替换为实际值展示,当用户修改data属性中的值时视图也会同步更新,接下来是另外一块核心部分,也就是分流处理的另一分支,节点编译,本篇涉及节点属性遍历、指令解析和事件处理~

先上一张我们在《Vue底层学习4——编译器框架搭建》中抛出的编译器流程图,本篇会严格按照这个流程进行编译器的功能编码,=。=我确实比较喜欢看图说话:

属性遍历

之前在compile的编译函数中,对Dom子节点遍历时如果子节点类型是Element,我们预留了一个编译节点的分支,接下来的重点就是在该分支中进行节点属性的遍历,找到v-@开头的属性并实现各自的处理流程。

/*** compile.js ***/
// new Compile(el, vm)

class Compile{
  constructor(el, vm) {...}

  // 提取指定Dom节点中的代码片段
  node2Fragment(el) {...}

  // 编译过程
  compile(el) {
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach(node => {
      // 类型判断
      if (this.isElement(node)) {
        // 节点属性遍历
        const nodeAttrs = node.attributes;
        Array.from(nodeAttrs).forEach(attr => {
          // 属性名
          const attrName = attr.name;
          // 属性值
          const exp = attr.value;
          if(this.isDirective(attrName)) {
            // 如果是v-开头的指令
          }
          if (this.isEvent(attrName)) {
            // 如果是@开头的事件
          }
        })
      } else if(this.isInterpolation(node)) {
        // 编译插值文本
        this.compileText(node);
      }

      // 递归子节点
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node);
      }
    })
  }

  // 是否是节点
  isElement(node) {...}
  // 是否是插值文本
  isInterpolation(node) {...}

  // 更新函数
  update(node, vm, exp, dir) {...}

  // 插值文本更新
  textUpdater(node, value) {...}

  // 插值文本编译
  compileText(node) {...}
}

通过isDirectiveisEvent方法分别判断是否是v-@开头的属性:

  // 是否是指令
  isDirective(name) {
    return name.indexOf('v-') === 0;
  }

  // 是否是事件
  isEvent(name) {
    return name.indexOf('@') === 0;
  }

指令解析

对于v-开头的指令我们根据指令名称标识抽象出需要调用的解析方法:

if(this.isDirective(attrName)) {
  // 如果是v-开头的指令
  const dir = attrName.substring(2);
  this[dir] && this[dir](node, this.$vm, exp);
}

针对不同类型的指令调用不同的方法,实现对应的功能:

  • v-text
text(node, vm, exp) {
  this.update(node, vm, exp, 'text');
}

// 与插值文本更新调用的是同一个函数
textUpdater(node, value) {
  node.textContent = value;
}

运行结果如下,可以看到通过v-text指令实现了name插值,视图中新增了一行name的展示,与此同时,created中对name的修改也同样生效:

  • v-html
  html(node, vm, exp) {
    this.update(node, vm, exp, 'html');
  }
  
    htmlUpdater(node, value) {
    node.innerHTML = value;
  }

运行结果如下,可以看到html属性的button被插入到了v-html所在到Dom节点中:

  • v-model
    该指令是Vue双向绑定的关键体现,可以理解为它是:value@input的结合体:
  // 指令v-model,双向绑定
  model(node, vm, exp) {
    // 指定input的value属性(值对视图的影响——MV)
    this.update(node, vm, exp, 'model');

    // input事件监听并修改数据模型中的值(视图对值对影响——VM)
    node.addEventListener('input', e => {
      vm[exp] = e.target.value;
    })
  }
  
  modelUpdater(node, value) {
    node.value = value;
  }

运行结果如下,name属性的值会以input当前的value值进行展示,与此同时,inputvalue的修改都会同步到一切使用name属性的视图中,这就实现了双向绑定:

事件处理

对于@开头的事件我们根据事件名称抽象出需要调用的处理方法:

if (this.isEvent(attrName)) {
  // 如果是@开头的事件
  const dir = attrName.substring(1);
  this.eventHandler(node, this.$vm, exp, dir);
}

具体的事件处理放到eventHandler中实现,实际就是给指定的节点绑定对应的事件监听,并且将回调函数的this指向当前的Vue实例,原因跟之前一样,这样就可以在回调函数中通过this轻松的获取到挂载到Vue实例上的属性:

  // 事件处理
  eventHandler(node, vm, exp, dir) {
    // 事件触发后需要执行的函数
    const fn = vm.$options.methods && vm.$options.methods[exp];
    if(dir && fn) {
      node.addEventListener(dir, fn.bind(vm));
    }
  }

运行结果如下,点击“改名儿”按钮时,namelocation的值被修改,视图也同步更新:

总结

手撸Vue核心代码就这样悄悄的结束了,心情舒畅,好像打通了任督二脉~最后做个小小的总结,整体下来我觉得Vue核心代码中最重要的部分就是编译,编译可以实现依赖收集,这样可以建立视图和数据模型之间的联系,当数据模型变更时,可以通知依赖对应数据的视图进行更新,也就是我们常说的数据驱动视图。而Vue中著名的双向绑定其实是通过v-model指令实现,在编译时解析该指令,为所属节点添加事件监听,事件触发时就可以即时修改绑定的值,绑定值的修改又会触发所有依赖的视图更新。

参考资料

1、Vue源码:https://github.com/vuejs/vue

posted @ 2021-08-23 14:30  Dreamsqin  阅读(275)  评论(0编辑  收藏  举报