热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

手动实现vue2.0的双向数据绑定原理详解

这篇文章主要给大家介绍了关于手动实现vue2.0的双向数据绑定原理的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

一句话概括:数据劫持(Object.defineProperty)+发布订阅模式

双向数据绑定有三大核心模块(dep 、observer、watcher),它们之间是怎么连接的,下面来一一介绍。

为了大家更好的理解双向数据绑定原理以及它们之间是如何实现关联的,先带领大家复习一下发布订阅模式。

一.首先了解什么是发布订阅模式

直接上代码:

一个简单的发布订阅模式,帮助大家更好的理解双向数据绑定原理

//发布订阅模式
function Dep() {
  this.subs = []//收集依赖(也就是手机watcher实例),
}
Dep.prototype.addSub = function (sub) { //添加订阅者
  this.subs.push(sub); //实际上添加的是watcher这个实例
}
Dep.prototype.notify = function (sub) { //发布,这个方法的作用是遍历数组,让每个订阅者的update方法去执行
  this.subs.forEach((sub) => sub.update())
}

function Watcher(fn) {
  this.fn = fn;
}
Watcher.prototype.update = function () { //添加一个update属性让每一个实例都可以继承这个方法
  this.fn();
}
let watcher = new Watcher(function () {
  alert(1)
});//订阅
let dep = new Dep();
dep.addSub(watcher);//添加依赖,添加订阅者
dep.notify();//发布,让每个订阅者的update方法执行

二.new Vue()的时候做了什么?

只是针对双向数据绑定做说明



Vue构造函数都干什么了?

function Vue(optiOns= {}) {
  this.$optiOns= options;//接收参数
  var data = this._data = this.$options.data;
  observer(data);//对data中的数据进型循环递归绑定
  for (let key in data) {
    let val = data[key];
    observer(val);
    Object.defineProperty(this, key, {
      enumerable: true,
      get() {
        return this._data[key];
      },
      set(newVal) {
        this._data[key] = newVal;
      }
    })
  }
  new Compile(options.el, this)
};

在new Vue({…})构造函数时,首先获取参数options,然后把参数中的data数据赋值给当前实例的_data属性上(this._data = this.$options.data),重点来了,那下面的遍历是为什么呢?首先我们在操作数据的时候是this.word获取,而不是this._data.word,所以是做了一个映射,在获取数据的时候this.word,其实是获取的this._data.word的值,大家可以在自己项目中输出this查看一下

1.接下来看看observer方法干了什么

function observer(data) {
  if (typeof data !== "object") return;
  return new Observer(data);//返回一个实例
}
function Observer(data) {
  let dep = new Dep();//创建一个dep实例
  for (let key in data) {//对数据进行循环递归绑定
    let val = data[key];
    observer(val);
    Object.defineProperty(data, key, {
      enumerable: true,
      get() {
        Dep.target && dep.depend(Dep.target);//Dep.target就是Watcher的一个实例
        return val;
      },
      set(newVal) {
        if (newVal === val) {
          return;
        }
        val = newVal;
        observer(newVal);
        dep.notify() //让所有方法执行
      }
    })
  }
}

Observer构造函数,首先let dep=new Dep(),作为之后的触发数据劫持的get方法和set方法时,去收集依赖和发布时调用,主要的操作就是通过Object.defineProperty对data数据进行循环递归绑定,使用getter/setter修改其默认读写,用于收集依赖和发布更新。

2.再来看看Compile具体干了那些事情

function Compile(el, vm) {
  vm.$el = document.querySelector(el);
  let fragment = document.createDocumentFragment(); //创建文档碎片,是object类型
  while (child = vm.$el.firstChild) {
    fragment.appendChild(child);
  };//用while循环把所有节点都添加到文档碎片中,之后都是对文档碎片的操作,最后再把文档碎片添加到页面中,这里有一个很重要的特性是,如果使用appendChid方法将原dom树中的节点添加到fragment中时,会删除原来的节点。
  replace(fragment);

  function replace(fragment) {
    Array.from(fragment.childNodes).forEach((node) => {//循环所有的节点
      let text = node.textContent;
      let reg = /\{\{(.*)\}\}/;
      if (node.nodeType === 3 && reg.test(text)) {//判断当前节点是不是文本节点且符不符合{{obj.text}}的输出方式,如果满足条件说明它是双向的数据绑定,要添加订阅者(watcher)
        console.log(RegExp.$1); //obj.text
        let arr = RegExp.$1.split("."); //转换成数组的方式[obj,text],方便取值
        let val = vm;
        arr.forEach((key) => { //实现取值this.obj.text
          val = val[key];
        });
        new Watcher(vm, RegExp.$1, function (newVal) {
          node.textCOntent= text.replace(/\{\{(.*)\}\}/, newVal)
        });
        node.textCOntent= text.replace(/\{\{(.*)\}\}/, val); //对节点内容进行初始化的赋值
      }
      if (node.nodeType === 1) { //说明是元素节点
        let nodeAttrs = node.attributes;
        Array.from(nodeAttrs).forEach((item) => {
          if (item.name.indexOf("v-") >= 0) {//判断是不是v-model这种指令
            node.value = vm[item.value]//对节点赋值操作
          }
          //添加订阅者
          new Watcher(vm, item.value, function (newVal) {
            node.value = vm[item.value]
          });
          node.addEventListener("input", function (e) {
            let newVal = e.target.value;
            vm[item.value] = newVal;
          })
        })
      }
      if (node.childNodes) { //这个节点里还有子元素,再递归
        replace(node);
      }
    })
  }

  //这是页面中的文档已经没有了,所以还要把文档碎片放到页面中
  vm.$el.appendChild(fragment);

}

Compile(编译方法)

首先解释一下DocuemntFragment(文档碎片)它是一个dom节点收容器,当你创造了多个节点,当每个节点都插入到文档当中都会引发一次回流,也就是说浏览器要回流多次,十分耗性能,而使用文档碎片就是把多个节点都先放入到一个容器中,最后再把整个容器直接插入就可以了,浏览器只回流了1次。

Compile方法首先遍历文档碎片的所有节点,1.判断是否是文本节点且符不符合{{obj.text}}的双大括号的输出方式,如果满足条件说明它是双向的数据绑定,要添加订阅者(watcher),new Watcher(vm,动态绑定的变量,回调函数fn) 2.判断是否是元素节点且属性中是否含有v-model这种指令,如果满足条件说明它是双向的数据绑定,要添加订阅者(watcher),new Watcher(vm,动态绑定的变量,回调函数fn) ,直至遍历完成。

最后别忘了把文档碎片放到页面中

3.Dep构造函数(怎么收集依赖的)

var uid=0;
//发布订阅
function Dep() {
  this.id=uid++;
  this.subs = [];
}
Dep.prototype.addSub = function (sub) { //订阅
  this.subs.push(sub); //实际上添加的是watcher这个实例
}
Dep.prototype.depend = function () { // 订阅管理器
  if(Dep.target){//只有Dep.target存在时采取添加
    Dep.target.addDep(this);
  }
}
Dep.prototype.notify = function (sub) { //发布,遍历数组让每个订阅者的update方法去执行
  this.subs.forEach((sub) => sub.update())
}

Dep构造函数内部有一个id和一个subs,id=uid++ ,id用于作为dep对象的唯一标识,subs就是保存watcher的数组。depend方法就是一个订阅的管理器,会调用当前watcher的addDep方法添加订阅者,当触发数据劫持(Object.defineProperty)的get方法时会调用Dep.target && dep.depend(Dep.target)添加订阅者,当数据改变时触发数据劫持(Object.defineProperty)的set方法时会调用dep.notify方法更新操作。

4.Watcher构造函数干了什么

function Watcher(vm, exp, fn) {
  this.fn = fn;
  this.vm = vm;
  this.exp = exp //
  this.newDeps = [];
  this.depIds = new Set();
  this.newDepIds = new Set();
  Dep.target = this; //this是指向当前(Watcher)的一个实例
  let val = vm;
  let arr = exp.split(".");
  arr.forEach((k) => { //取值this.obj.text
    val = val[k] //取值this.obj.text,就会触发数据劫持的get方法,把当前的订阅者(watcher实例)添加到依赖中
  });
  Dep.target = null;
}
Watcher.prototype.addDep = function (dep) {
  var id=dep.id;
  if(!this.newDepIds.has(id)){
    this.newDepIds.add(id);
    this.newDeps.push(dep);
    if(!this.depIds.has(id)){
      dep.addSub(this);
    }
  }
 
}
Watcher.prototype.update = function () { //这就是每个绑定的方法都添加一个update属性
  let val = this.vm;
  let arr = this.exp.split(".");
  arr.forEach((k) => { 
    val = val[k] //取值this.obj.text,传给fn更新操作
  });
  this.fn(val); //传一个新值
}

Watcher构造函数干了什么

1 接收参数,定义了几个私有属性( this.newDep ,this.depIds
,this.newDepIds)

2. Dep.target = this,通过参数进行data取值操作,这就会触发Object.defineProperty的get方法,它会通过订阅者管理器(dep.depend())添加订阅者,添加完之后再将Dep.target=null置为空;

3.原型上的addDep是通过id这个唯一标识,和几个私有属性的判断防止订阅者被多次重复添加

4.update方法就是当数据更新时,dep.notify()执行,触发订阅者的update这个方法, 执行发布更新操作。

总结一下

vue2.0中双向数据绑定,简单来说就是Observer、Watcher、Dep三大部分;

1.首先用Object.defineProperty()循环递归实现数据劫持,为每个属性分配一个订阅者集合的管理数组dep;

2.在编译的时候,创建文档碎片,把所有节点添加到文档碎片中,遍历文档碎片的所有结点,如果是{{}},v-model这种,new Watcher()实例并向dep的subs数组中添加该实例

3.最后修改值就会触发Object.defineProperty()的set方法,在set方法中会执行dep.notify(),然后循环调用所有订阅者的update方法更新视图。

到此这篇关于手动实现vue2.0的双向数据绑定原理的文章就介绍到这了,更多相关vue2.0双向数据绑定内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!


推荐阅读
  • 2017年软件开发领域的七大变革
    随着技术的不断进步,2017年对软件开发人员而言将充满挑战与机遇。本文探讨了开发人员需要适应的七个关键变化,包括人工智能、聊天机器人、容器技术、应用程序版本控制、云测试环境、大众开发者崛起以及系统管理的云迁移。 ... [详细]
  • 本文详细介绍如何在华为鲲鹏平台上构建和使用适配ARM架构的Redis Docker镜像,解决常见错误并提供优化建议。 ... [详细]
  • DirectShow Filter 开发指南
    本文总结了 DirectShow Filter 的开发经验,重点介绍了 Source Filter、In-Place Transform Filter 和 Render Filter 的实现方法。通过使用 DirectShow 提供的类,可以简化 Filter 的开发过程。 ... [详细]
  • 如何在DedeCMS专题页节点文档中调用自定义模型字段?
    在完成DedeCMS专题页节点文章列表样式的修改后,如果需要在列表中显示自定义模型的字段,由于DedeCMS默认不支持这一功能,因此需要进行一些二次开发。本文将详细介绍如何通过修改模板文件和核心文件来实现这一需求。 ... [详细]
  • 本文详细介绍了CSS中元素的显示模式,包括块元素、行内元素和行内块元素的特性和应用场景。 ... [详细]
  • 本文介绍了如何将Spring属性占位符与Jersey的@Path和@ApplicationPath注解结合使用,以便在资源路径中动态解析属性值。 ... [详细]
  • ABP框架是ASP.NET Boilerplate的简称,它不仅是一个开源且文档丰富的应用程序框架,还提供了一套基于领域驱动设计(DDD)的最佳实践架构模型。本文将详细介绍ABP框架的特点、项目结构及其在Web API优先架构中的应用。 ... [详细]
  • 作为一名新手开发者,我正在尝试使用 ASP.NET 和 Vue.js 构建一个单页面应用,涉及多个复杂组件(如按钮、图表等)。希望有经验的开发者能够提供指导。 ... [详细]
  • 深入理解Java多线程与并发机制
    本文探讨了Java多线程和并发机制的核心概念,包括多线程类的分类、执行器框架、并发容器及控制工具。通过详细解析这些组件,帮助开发者更好地理解和应用多线程技术。 ... [详细]
  • Spring 中策略模式的应用:Resource 接口详解
    本文探讨了在 Spring 框架中如何利用 Resource 接口实现资源访问策略。Resource 接口作为资源访问策略的抽象,通过多种实现类支持不同类型的资源访问。 ... [详细]
  • Java EE 平台集成了多种服务、API 和协议,旨在支持基于 Web 的多层应用程序开发。本文将详细介绍 Java EE 中的 13 种关键技术规范,帮助开发者更好地理解和应用这些技术。 ... [详细]
  • vue引入echarts地图的四种方式
    一、vue中引入echart1、安装echarts:npminstallecharts--save2、在main.js文件中引入echarts实例:  Vue.prototype.$echartsecharts3、在需要用到echart图形的vue文件中引入:   importechartsfrom"echarts";4、如果用到map(地图),还 ... [详细]
  • 面试题总结_2019年全网最热门的123个Java并发面试题总结
    面试题总结_2019年全网最热门的123个Java并发面试题总结 ... [详细]
  • 使用Tkinter构建51Ape无损音乐爬虫UI
    本文介绍了如何使用Python的内置模块Tkinter来构建一个简单的用户界面,用于爬取51Ape网站上的无损音乐百度云链接。虽然Tkinter入门相对简单,但在实际开发过程中由于文档不足可能会带来一些不便。 ... [详细]
  • 本文介绍了 Python 中的基本数据类型,包括不可变数据类型(数字、字符串、元组)和可变数据类型(列表、字典、集合),并详细解释了每种数据类型的使用方法和常见操作。 ... [详细]
author-avatar
一巴掌呼死你
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有