Vue.js的核心功能有两个:一是响应式的数据绑定系统,二是组件系统。本文是通过学习他人的文章,从而理解了双向绑定原理,从而在自己理解的基础上,自己动手实现数据的双向绑定。

  目前几种主流的mvc(vm)框架都实现了单向数据绑定,而双向数据绑定我觉得就是在单向绑定的基础上,给input、textarea等可输入元素添加change事件,从而动态修改model和view。我们可以动手做一个。

  1. <body>
  2. <input type="text" id="test">
  3. <span id="show"></span>
  4. <script type="text/javascript">
  5. let obj = {};
  6. Object.defineProperty(obj,'name',{
  7. configurable: false,
  8. enumerable: true,
  9. get: function() {
  10. return val
  11. },
  12. set: function(newVal) {
  13. document.getElementById('test').value = newVal;
  14. document.getElementById('show').innerHTML = newVal;
  15. }
  16. })
  17. document.addEventListener('keyup',function(e) {
  18. obj.name = e.target.value
  19. })
  20. </script>
  21. </body>

此时的效果是:在input中输入内容,会同步显示到span中;在控制台中修改输入obj.name的值,视图也会得到相应的更新。这样就实现了一个简单的数据双向绑定。

  Vue.js是通过数据劫持结合发布者-订阅者的方式,通过Object.defineProperty()来劫持各个属性的getter、setter,在数据发生变动时发布消息给订阅者,同时触发相应的监听回调。

  整理了一下实现数据双向绑定的思路:

  1.实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如果有变动可以拿到最新值并通知订阅者;

  2.实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定到相应的更新函数

  3.实现一个Watcher,作为Observer和Compile的桥梁,能够订阅并接收每个属性变动的通知,执行绑定回调函数相应的更新函数,从而更新视图

  4.入口函数Vue

以下是我们需要实现的:

  1. <div id="app">
  2. <input type="text" v-model="name">
  3. <p>{{name}}</p>
  4. <p v-text="name"></p>
  5. {{name}}
  6. </div>
  7. <script>
  8. let vm = new Vue({
  9. el: 'app',
  10. data: {
  11. name: 'zengfp'
  12. }
  13. })
  14. </script>

实现Observer

我们知道可以利用Object.defineProperty()来监听属性的变动,那么我们将需要用observer对数据对象进行递归遍历,包括子属性对象的属性,(但是在这里,我做的比较简单,所以就不需要对进行递归遍历了。)都加上setter和getter。如果给这个对象赋值,就会触发setter函数,那么就能监听到数据变化。相关代码如下:

  1. function Observe(obj){
  2. if(!obj || typeof obj !== 'object'){
  3. return
  4. }
  5. Object.keys(obj).forEach(function(key) {
  6. defineReactive(obj,key,obj[key])
  7. })
  8. }
  9.  
  10. function defineReactive(obj,key,val){
  11. Object.defineProperty(obj,key,{
  12. configurable: false,
  13. enumerable: true,
  14. get: function() {
  15. return val
  16. },
  17. set: function(newVal) {
  18. val = newVal;
  19. console.log('我的数据发生了改变:'+ val + '->' + newVal)
  20. }
  21. })
  22. }

这样我们就可以监听到每个数据的变化了,接下来就是在监听到数据变化之后怎么通知订阅者了,所有接下来需要实现一个消息订阅器,用一个简单的数组,作为订阅者收集器,数据变动触发notify(),在调用订阅者的update()方法,代码改善后如下:

  1.      function Dep() {
  2. this.subs = []
  3. }
  4. Dep.prototype = {
  5. addSub: function(sub) {
  6. this.subs.push(sub)
  7. },
  8. notify: function() {
  9. this.subs.forEach(function(sub) {
  10. sub.update()
  11. })
  12. }
  13. }
  1.     function defineReactive(obj,key,val){
            let dep = new Dep()
  2. Object.defineProperty(obj,key,{
  3. configurable: false,
  4. enumerable: true,
  5. get: function() {
  6. return val
  7. },
  8. set: function(newVal) {
  9. val = newVal;
  10. console.log('我的数据发生了改变:'+ val + '->' + newVal              
                  dep.notify()// 通知所有订阅者
               
    }
            })
        }
  1.  

上面的思路整理中我们已经明确订阅者应该是Watcher, 而且let dep = new Dep();是在defineReactive方法内部定义的,所以想通过dep添加订阅者,就必须要在闭包内操作,所以我们可以在 getter里面动手脚:

  1.       Object.defineProperty(obj,key,{
  2. ......
  3. get: function() {
                //由于需要在闭包内添加watcher,所以通过dep定义一个全局属性target,暂存于watcher,添加完移除
  4. Dep.target && dep.addSub(Dep.target)
  5. return val
  6. },
  7. .......
  8. })

至此就实现了一个observer,接下来需要实现Compile了

实现Compile

compile主要做的事情就是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,就会收到通知更新视图,因为遍历解析的时候有多次操作dom节点,为提高性能和效率,会先将根节点el转换成文档碎片documentFragment进行解析编译操作,待解析完成,在将documentFragment添加到真实的dom节点中。

  1.     function Compile(el,vm) {
  2. this.$vm = vm;
  3. this.$el = document.getElementById(el)
  4. if(this.$el){
  5. this.$fragment = this.nodeToFragment(this.$el);
  6. this.compile(this.$fragment);
  7. this.$el.appendChild(this.$fragment)
  8. }
  9. }
      
  1. Compile.prototype = {
  2. isElementNode: function(node) {
  3. return node.nodeType == 1
  4. },
  5. isTextNode: function(node) {
  6. return node.nodeType == 3
  7. },
  8. nodeToFragment: function(node) {
  9. let fragment = document.createDocumentFragment() , child;
  10. while(child = node.firstChild) {
  11. fragment.appendChild(child)
  12. }
  13. return fragment
  14. },
  15. compile: function(node) {
  16. let childNodes = node.childNodes,
  17. _this = this;
  18. [].slice.call(childNodes).forEach(function(node) {
  19. let value = node.textContent,
  20. reg = /\{\{(.*)\}\}/;
  21. if(_this.isElementNode(node)){
  22. _this.compileElement(node)//元素节点
  23. }else if(_this.isTextNode(node) && reg.test(value)) {
  24. _this.compileText(node,RegExp.$1)//文本节点
  25. }
  26. })
  27. },
  28. compileElement: function(node) {
  29. let attrs = node.attributes,
  30. value = node.textContent,
  31. reg = /\{\{(.*)\}\}/,
  32. _this = this;
  33. if(attrs && attrs.length >0){
  34. [].slice.call(attrs).forEach(function(attr) {
  35. let attrName = attr.name;
  36. if(attrName == 'v-model'){
  37. let key = attr.value;
  38. node.addEventListener('input',function(e) {
  39. _this.$vm.data[key] = e.target.value;
  40. })
  41. node.value = _this.$vm.data[key]
  42. }else if(attrName == 'v-text'){
  43. let key = attr.value
  44. // node.textContent = _this.$vm.data[key]
  45. new Watcher(_this.$vm,node,key)
  46. }
  47. node.removeAttribute(attrName)
  48. })
  49. }else if(reg.test(value)){
  50. let key = RegExp.$1;
  51. new Watcher(_this.$vm,node,key)
  52. }
  53. },
  54. compileText: function (node,key) {
  55. new Watcher(this.$vm,node,key)
  56. }
  57. }

实现Watcher

watcher订阅者作为observer和compile之间通信的桥梁,主要做以下事情:

在自身实例化时往属性订阅器中dep中添加自己;

自身有一个update的方法

待属性自身变动dep.notify通知时,能够调用update方法,并触发compile中的回调

  1. function Watcher(vm,node,key){
  2. Dep.target = this;//将当前订阅器指向自己
  3. this.key = key;
  4. this.node = node;
  5. this.vm = vm;
  6. this.update();//触发get()函数,从而在dep中添加自己:Dep.target && dep.addSub(Dep.target)
  7. Dep.target = null
  8. }
  9. Watcher.prototype = {
  10. update: function () {
  11. this.get();
  12. this.node.textContent = this.value;
  13. },
  14. get: function() {
  15. this.value = this.vm.data[this.key]
  16. }
  17. }

实现Vue

  1. function Vue(options) {
  2. this.data = options.data;
  3. let data = this.data;
  4. observe(data);
  5. let id = options.el;
  6. this.compile = new Compile(id || document.body, this)
  7. }

总结

本文主要是让自己更加理解双向绑定原理。

observe每个数据的属性,object.defineProperty(),setter、getter函数 ——>在compile的时候为每个元素节点添加订阅者watcher——>添加watcher的过程中触发了update函数,进而调用watcher里面的get函数——>从而触发了访问器里面的get函数,从而触发了订阅器中的addSub函数,就把watcher添加到订阅器中——>从而实现了model->view的实现;

当在input输入框中输入文本,触发了input的监听事件,把input中的值赋给observe中的对象属性——>从而触发了Object.defineProperty()的set函数,从而消息订阅器发出通知dep.notify()——>从而每个订阅者执行更新函数sub.update(),从而触发watcher的get函数——>获取最新值时又触发了访问器里面的get函数——>从而更新最新值到视图中,实现了view->model

Vue之双向绑定原理动手记的更多相关文章

  1. vue的双向绑定原理及实现

    前言 使用vue也好有一段时间了,虽然对其双向绑定原理也有了解个大概,但也没好好探究下其原理实现,所以这次特意花了几晚时间查阅资料和阅读相关源码,自己也实现一个简单版vue的双向绑定版本,先上个成果图 ...

  2. vue数据双向绑定原理

    vue的数据双向绑定的小例子: .html <!DOCTYPE html> <html> <head> <meta charset=utf-> < ...

  3. vue的双向绑定原理解析(vue项目重构二)

    现在的前端框架 如果没有个数据的双向/单向绑定,都不好意思说是一个新的框架,至于为什么需要这个功能,从jq或者原生js开始做项目的前端工作者,应该是深有体会. 以下也是个人对vue的双向绑定原理的一些 ...

  4. vue的双向绑定原理浅析与简单实现

    很久之前看过vue的一些原理,对其中的双向绑定原理也有一定程度上的了解,只是最近才在项目上使用vue,这才决定好好了解下vue的实现原理,因此这里对vue的双向绑定原理进行浅析,并做一个简单的实现. ...

  5. Vue数据双向绑定原理及简单实现

    嘿,Goodgirl and GoodBoy,点进来了就看完点个赞再go. Vue这个框架就不简单介绍了,它最大的特性就是数据的双向绑定以及虚拟dom.核心就是用数据来驱动视图层的改变.先看一段代码. ...

  6. 西安电话面试:谈谈Vue数据双向绑定原理,看看你的回答能打几分

    最近我参加了一次来自西安的电话面试(第二轮,技术面),是大厂还是小作坊我在这里按下不表,先来说说这次电面给我留下印象较深的几道面试题,这次先来谈谈Vue的数据双向绑定原理. 情景再现: 当我手机铃声响 ...

  7. Vue.js双向绑定原理

    Vue.js最核心的功能有两个,一个是响应式的数据绑定系统,另一个是组件系统.本文仅仅探究双向绑定是怎样实现的.先讲涉及的知识点,再用简化的代码实现一个简单的hello world示例. 一.访问器属 ...

  8. 探讨vue的双向绑定原理及实现

    1.vue的实现原理 vue的双向绑定是由数据劫持结合发布者-订阅者模式实现的,那么什么是数据劫持?vue是如何进行数据劫持的?说白了就是通过Object.defineProperty()来劫持对象属 ...

  9. 【Vue】vue的双向绑定原理及实现

    vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的,那么vue是如果进行数据劫持的,我们可以先来看一下通过控制台输出一个定义在vue初始化数据上的对象是个什么东西. 代码: var ...

随机推荐

  1. [SCOI2014]方伯伯的商场之旅

    Description 方伯伯有一天去参加一个商场举办的游戏.商场派了一些工作人员排成一行.每个人面前有几堆石子.说来也巧,位置在 i 的人面前的第 j 堆的石子的数量,刚好是 i 写成 K 进制后的 ...

  2. Win10新增功能快捷键大全

    原文地址:http://wenwen.sogou.com/z/q703976788.htm贴靠窗口:Win + 左/右 > Win + 上/下 > 窗口可以变为 1/4 大小放置在屏幕 4 ...

  3. LINQ 查询

    概述 事实上,对于LINQ to Objects来说,就是通过为IEnumerable<T>接口定义了一组约50个扩展方式来实现的. Lambda表达式(拉姆达表达式,Lambda Exp ...

  4. 【原创】backbone1.1.0源码解析之View

    作为MVC框架,M(odel)  V(iew)  C(ontroler)之间的联系是必不可少的,今天要说的就是View(视图) 通常我们在写逻辑代码也好或者是在ui组件也好,都需要跟dom打交道,我们 ...

  5. js调试系列: 控制台命令行API

    js调试系列目录: - 上次初步介绍了什么是控制台,以及简单的 console.log 输出信息.最后还有两个小问题,我们就当回顾,来看下怎么操作吧. 先打开百度,然后按 F12 打开后,如果不是 C ...

  6. Web性能优化系列(2):剖析页面绘制时间

    本文由 伯乐在线 - J.c 翻译,sunbiaobiao 校稿.未经许可,禁止转载!英文出处:www.deanhume.com.欢迎加入翻译小组. 最近,我参加了在伦敦举办的Facebook移动开发 ...

  7. Linux - sed 文本操作

    SED 是一项Linux指令,功能同awk类似,差别在于,sed简单,对列处理的功能要差一些,awk的功能复杂,对列处理的功能比较强大. sed全称是:Stream EDitor 调用sed命令有两种 ...

  8. python字典转datafarm,pandas

    # coding:utf-8 import json import pandas as pd with open("./article_file/all_article.json" ...

  9. git 修改已提交的注释

    在git中,其commit提供了一个--amend参数,可以修改最后一次提交的信息 修改最后一次提交注释 git commit --amend 然后在出来的编辑界面,直接编辑注释的信息,保存退出 gi ...

  10. 洛谷 P4609: [FJOI2016] 建筑师

    本省省选题是需要做的. 题目传送门:洛谷P4609. 题意简述: 求有多少个 \(1\) 到 \(N\) 的排列,满足比之前的所有数都大的数正好有 \(A\) 个,比之后的所有数都大的数正好有 \(B ...