Vue数据绑定原理及简单实现

本篇文章中的代码只是部分片段,完整代码存放于github上https://github.com/Q-Zhan/simple-vue

进入正文~实现数据绑定主要是要实现两个方面的功能:数据变化导致视图变化,视图变化导致数据变化。后者比较容易实现,就是监听视图的事件,然后在回调函数中改变数据。所以重点是数据变化时如何改变视图。
这里的思路是通过object.defineProperty()来对数据的属性设置一个set函数,设置后当数据改变时set函数就会被调用,我们就可以里面进行视图更新操作。


具体实现过程


如上图所示,我们需要一个监听器Observer来给所有的属性设置set函数。如果属性发生了变化,就要通知所有的订阅者Watcher。而这些Watcher统一存放在消息订阅器Dep中,这样比较方便统一管理。Watcher接受到来自Dep的通知后就执行相应的操作去更新视图。

Observer

监听器的核心代码如下:

function observe(data) {
  if (!data || typeof data !== 'object') {
    return;
  }
  Object.keys(data).forEach(function(key) {  // 遍历属性,递归设置set函数
    defineReactive(data, key, data[key]);
  });
}
function defineReactive(data, key, val) {
  observe(val)
  var dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      if (Dep.target) {
        dep.addSub(Dep.target)  // 添加watcher
      }
      return val
    },
    set: function(newVal) {
      if (val === newVal) {
        return;
      }
      val = newVal;
      dep.notify()  // 通知dep
    }
  })
}

通过调用observe()函数来递归地给data对象设置set和get函数,在data的属性被get时添加watcher,被set时通知dep,dep的notify会接着通知所有的watcher去执行更新操作。

Dep

消息订阅器的核心代码如下:

function Dep() {
  this.subs = []  // 订阅者数组
}
Dep.prototype = {
  addSub: function(sub) {
    this.subs.push(sub)
  },
  notify: function() {
    this.subs.forEach(function(sub) {
      sub.update()
    })
  }
}
Dep.target = null

消息订阅器比较简单,就是维护一个subs数组。当监听新属性时把它push进subs数组中,然后dep被通知时触发notify函数,从而触发subs数组中每个watcher的update操作。

Watcher

function Watcher(vm, exp, cb) {
  this.cb = cb
  this.vm = vm
  this.exp = exp
  this.value = this.get()
}

Watcher.prototype = {
  update: function() {
    this.run()
  },
  run: function() {
    var value = this.vm.data[this.exp]
    var oldVal = this.value
    if (value !== oldVal) {
      this.value = value
      this.cb.call(this.vm, value, oldVal)  // 执行更新时的回调函数
    }
  },
  get: function() {
    Dep.target = this
    var value = this.vm.data[this.exp]  // 读取data的属性,从而执行属性的get函数
    Dep.target = null
    return value
  }
}

Watcher的主要功能是去触发属性的get函数,从而添加watcher到Dep的subs数组中。另外就是在update()中更新属性的值并触发更新回调函数。
使用Watcher的方法如下:

var el = document.getElementById('XXX')
observe(data)
new Watcher(vm, exp, function(value) {  // vm表示某个实例,exp表示属性名
  el.innerHTML = value
})

为了使用时的整洁,我们需要把代码稍微包装下。

SimpleVue

function SimpleVue (data, el, exp) {
  var self = this
  this.data = data
  Object.keys(data).forEach(function(key) {
    self.proxyKeys(key)
  })
  observe(data)
  el.innerHTML = this.data[exp]
  new Watcher(this, exp, function(value) {
    el.innerHTML = value
  })
  return this
}

SimpleVue.prototype = {
  proxyKeys: function(key) {
    var self = this
    Object.defineProperty(this, key, {
      enumerable: false,
      configurable: true,
      get: function() {
        return self.data[key]
      },
      set: function(newVal) {
        self.data[key] = newVal
      }
    })
  }
}

使用如下:

// html
<h1 id="name">{{name}}</h1>  //这个{{name}}暂时没用

// js
var el = document.querySelector('#name')
var selfVue = new SimpleVue({ name: 'hello'}, el, 'name')
setTimeout(function() {
  selfVue.name = '123'
}, 2000)

需要注意的是SimpleVue原型的proxyKeys是为了将selfVue.data.name这种操作代理为selfVue.name。这下我们就可以直接通过selfVue.name = "XXX"来改变数据了,并且视图也会相应变化。

Compile

上面的例子都是写死一个属性去替换,而真正的使用时我们需要去解析dom节点,对类如{{}}的进行替换并绑定watcher。这个解析过程通过Compile来实现。

nodeToFragement: function(el) {
    var fragment = document.createDocumentFragment()
    var child = el.firstChild
    // 将dom节点移到fragment
    while(child) {
      fragment.appendChild(child)
      child = el.firstChild
    }
    return fragment
  },
  compileElement: function(el) {
    var childNodes = el.childNodes
    var self = this;
    [].slice.call(childNodes).forEach(function(node) {
      var reg = /\{\{(.*)\}\}/
      var text = node.textContent
      if (self.isTextNode(node) && reg.test(text)) {
        self.compileText(node, reg.exec(text)[1])
      }
      if (node.childNodes && node.childNodes.length) {
        self.compileElement(node)  // 递归遍历子节点
      }
    });
  },
  compileText: function(node, exp) {
    var self = this
    var initText = this.vm[exp]
    this.updateText(node, initText)
    new Watcher(this.vm, exp, function(value) {
      self.updateText(node, value)
    })
  },

compile主要做三件事情。一是将dom节点移入DocumentFragment中去,因为DocumentFragment中操作dom节点不会引起浏览器的重绘,性能会比直接操作dom节点好很多。二是递归调用compileElement函数来遍历所有子节点,如果子节点包含{{}}形式的则调用compileText。三是compileText函数创建新的watcher。

当然加入compile后SimpleVue也要有相应的变化:

function SimpleVue (options) {
  var self = this
  this.vm = this
  this.data = options.data
  Object.keys(this.data).forEach(function(key) {
    self.proxyKeys(key)
  })
  observe(this.data)
  new Compile(options.el, this.vm)
  return this
}

[参考资料]:https://www.cnblogs.com/libin-1/p/6893712.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容