Vue实现原理

作者: Quilljou | 来源:发表于2017-07-26 23:43 被阅读501次

    Vue是目前炙手可热的JS框架,作为一个视图库,最重要的功能当然是数据绑定了,数据变化,模板变化。

    接下来让我看看Vue实现的大致原理是怎样的。

    这是我们的模板。

    <div id="app">
      <input type="text" v-model="name">
      <p>
        {{ name }}
      </p>
      <strong>{{ name }}</strong>
    </div>
    

    像下面这样实例化

    var vm = new Vue({
      el: "#app",
      data: {
        name: 'ok'
      }
    })
    

    我们先不讨论v-model指令。

    我们初始化实例时传入一个data。data的name属性对应模板中的name。

    我们要实现的功能点有

    • vm实例代理data上的属性。
    • 在vm对象内部,我们用this指代vm。当在vm内部this.name 被赋了一个新的值时,模板中的name也会同步变化。

    这是单向数据绑定。

    然后再考虑v-modelv-model相同于一个语法糖,监听了表单控件,用户输入。this.name也会变化。有了上面的条件,模板也会变化了。

    这就是双向数据绑定。

    那应该如何实现呢?

    构造函数

    function Vue(option) {
      let { el, data } = option;
      let node = document.querySelector(el);
      this.data = data;
    
      observe(data,this) // 代理绑定属性
    
      let dom = kidnap(node,this) // 遍历dom树,编译,返回一个新的dom树
    
      node.appendChild(dom)
    }
    

    代理绑定属性

    Vue对属性变化检测的核心实现就是Object.defineProperty方法。这个方法可以为对象定义新的属性。可以设置getter,setter回调。

    在这里的实践就是遍历data对象,data对象上面的每个属性被vm代理。当属性变化,setter回调,广播通知订阅者;getter被回调时,检测是否可以添加订阅者。

    function defineReactive(obj,key,val) {
      var dep = new Dep() // 为每一个属性实例一个发布者
      Object.defineProperty(obj,key,{
        get: function() {
          if(Dep.target) {
            dep.addSub(Dep.target); // 添加订阅者
          }
          return val
        },
        set: function (newVal) {
          if(newVal === obj[key]) return;
          val = newVal;
          dep.notify() // 当属性变化,广播通知订阅者
        }
      })
    }
    
    function observe(obj,vm) {
      Object.keys(obj).forEach(function (key) {
        defineReactive(vm,key,obj[key]);
      })
    }
    

    发布

    class Dep {
      constructor () {
        this.subs = []
      }
    
      addSub (sub) {
        this.subs.push(sub)
      }
    
      notify () {
        this.subs.forEach(item => {
          item.update()
        })
      }
    }  
    

    每一个发布者都维护了一个订阅者数组,发布者的notify方法会遍历所有订阅者,调用订阅者的update方法。所以每一个订阅者必须实现一个update方法。

    编译

    我们知道当vm上的属性变化时,所有的订阅者都会收到通知。那么这些订阅者是谁呢?

    订阅者就是模板中“Mustache” 语法(双大括号)的文本插值。

    首先我们要将DOM中劫持过来。

    function kidnap(node,vm) {
      if(!node) return;
      let frag = document.createDocumentFragment();
      while (child = node.firstChild) {
        frag.appendChild(child)
      }
      DFS(frag,function(node) {
        compile(node,vm)
      })
      return frag;
    }
    

    值得一提的是上面代码中的appendChild方法。
    DOM规定,一个DOM节点不能同属于两个父节点,所以对一个拥有父节点的节点执行appendChild其实是将它搬移到另一个节点。
    同时进行while循环,可以巧妙的搬运一个节点下的所有子节点。

    拿到模板之后对模板进行一些处理。

    function compile(node,vm) {
      if(!node) return;
      var reg = /\{\{(.*)\}\}/;
    
      if(node.nodeType == 1) {
        let attr = node.attributes;
        for (var i = 0; i < attr.length; i++) {
          if(node.tagName.toLowerCase() == "input" && attr[i].name == "v-model") {
            var name = attr[i].nodeValue;
            node.addEventListener("keyup",function (e) {
              vm[name] = e.target.value;
            })
            node.value = vm[name]
            node.removeAttribute("v-model")
            new Watcher(node,vm,name) // 订阅者
          }
        }
      }
    
      if(node.nodeType == 3) {
        if(reg.test(node.nodeValue)) {
          var name = RegExp.$1;
          name = name.trim();
          node.nodeValue = vm[name]
          new Watcher(node,vm,name) // 订阅者
        }
      }
    }
    

    遍历每一个DOM节点。这里用到了DFS,深度优先搜索。可以参见我的上一篇文章。代码如下。

    function DFS(node, cb) {
      let deep = 1;
      DFSdom(node,deep,cb)
    }
    
    function DFSdom(node, deep, cb) {
      if(!node)
        return;
    
      cb(node,deep)
    
      if(!node.childNodes.length) {
        return;
      }
    
      deep++;
    
      Array.from(node.childNodes).forEach(item => DFSdom(item,deep,cb))
    }
    

    订阅

    我们可以看到在遍历DOM树的时候,对符合我们语法条件的节点进行了watch。watcher相当于订阅者。

    class Watcher {
      constructor (node, vm, name) {
        Dep.target =  this;
        this.name = name;
        this.node = node;
        this.vm = vm;
        this.update()
        Dep.target = null;
      }
    
      update () {
        this.value = this.vm[this.name];
        this.node.nodeValue = this.value;
        if(this.node.nodeType == 1) {
          this.node.value = this.value
        }
      }
    }
    

    相关文章

      网友评论

        本文标题:Vue实现原理

        本文链接:https://www.haomeiwen.com/subject/gcsakxtx.html