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