前言
很多人在面试过程中都有问到Vue双向绑定的原理和实现,这是一个老生常谈的面试题了,虽然网上也有很多实现双向绑定的文章,但是我看后觉得对于大多数前端小白来说,不是很容易理解,所以,这篇文章我就用最简单的代码教大家怎么实现一个Vue的双向绑定。
双向绑定的原理
用过Vue框架的都知道,页面在初始化的时候,我们可以把data里的属性渲染到页面上,改动页面上的数据时,data里的属性也会相应的更新,这就是我们所说的双向绑定,所以,简单来说,我们要实现一个双向绑定要实现以下3点操作:
- 首先需要在Vue实例化的时候,解析代码中
v-modle
指令和{ {}}
指令,然后把data里的属性绑定到相应的指令上,所以我们要实现一个解析器Compile,这是第一点; - 接着我们在改变页面的属性的时候,要知道哪个属性改变了,这时候我们需要用到
Object.defineProperty
中的getter
和setter
方法对属性进行劫持,这里我们要实现一个监视器Observer,这是二点; - 我们在知道具体哪个属性改变后,要执行相应的函数,更新视图,这里我们要实现一个消息订阅,在页面初始化的时候订阅每个属性,并且在
Object.defineProperty
数据劫持的时候接收属性改变通知,更新视图,所以我们要实现一个订阅者Watcher,这是第三点。
1. 实现Compile
首先,我们从最基本的解析指令开始,话不多说,先上代码:
v-model
和 { {}}
指令,但是页面渲染的时候,我们在浏览器看到的节点是这样的。 v-model
和 { {}}
开始。 话不多说,上代码: MVVMdemo 复制代码{ {text}}
上面这段代码就是解析指令的简单方法,我来简单解释一下:
document.createDocumentFragment()
document.createDocumentFragment()
相当于一个空的容器, 是用来创建一个虚拟的节点对象,在这里我们要做的就是:在遍历节点的同时对相应指令进行解析,解析完一个指令将其添加到createDocumentFragment
中,解析完后再重新渲染页面,这样的好处就是减少页面渲染dom的次数,详细内容可参考文档function compile (node, vm)
compile()
方法里面我们对每个节点进行判断,首先判断节点是否包含有子节点,有的话继续调用compile()方法进行解析。没有的话就判断节点类型,我们主要是判断element元素类型
和文本text元素类型
,然后分别对这两种类型进行解析。
完成了以上步骤后,我们的代码就可以正常显示在页面上了, 但是,有一个问题,我们页面上绑定了data里的属性,但是在改变input框里的数据的时候,相应的data里面的数据没有同步更新。所以,接下来我们要对数据的更新进行劫持,通过Object.defineProperty()
劫持data里的对应属性变化。
2. 实现Observer
要实现数据的双向绑定,我们需要通过Object.defineProperty()来实现数据劫持,监听属性的变化。 所以,接下来我们先通过一个简单的例子来了解Object.defineProperty()
的工作原理。
var obj ={};var name="hello";Object.defineProperty(obj,'name',{ get:function(val) {//获取属性 console.log('get方法被调用了'); return name }, set:function(val) { //设置属性 console.log('set方法被调用了'); name=val }})console.log(obj.name);obj.name='hello world'console.log(obj.name);复制代码
运行代码,我们可以看到控制台输出:
Object.defineProperty( )
设置了对象obj的name属性,对其get和set进行重写操作,顾名思义,get就是在读取name属性这个值触发的函数,set就是在设置name属性这个值触发的函数,关于 Object.defineProperty()
这里就不多说了,具体可以参考文档 所以,接下来我们要做的是当我们在输入框输入数据的时候,首先触发 input 事件(或者 keyup、change 事件),在相应的事件处理程序中,我们获取输入框的 value 并赋值给 vm 实例的 text 属性。话不多说,上代码。 MVVMdemo 复制代码{ {text}}
我们在页面初始化的时候,通过递归遍历data所有子属性,给每个属性添加一个监视器,在监听到数据变化时候,就会触发defineProperty( )里的set方法,我们可以在控制台输出看到set方法里监听到属性的变化。
3. 实现Watcher
很多人看过网上的其他实现MVVM实现的代码,但是都说对Watcher订阅者不是很了解,其实抛开代码,Watcher实现的功能其实很简单,就是当Vue实例化的时候,给每个属性注入一个订阅者Watcher,方便在Object.defineProperty()
数据劫持中监听属性的获取(get方法),在Object.defineProperty()
监听到数据改变的时候(set方法),通过Watcher通知更新,所以简单来说,Watcher就是起到一个桥梁的作用。我们上面已经通过Object.defineProperty()
监听到数据的改变,接下来我们通过实现Watcher 来完成双向绑定的最后一步。
MVVMdemo 复制代码{ {text}}
我们在第二步的代码基础上,加了一个订阅者Watcher和一个消息收集器Dep,接下来我就跟大家说说他们都做了什么。 首先:
function Watcher(vm, node, name, nodeType) { Dep.target = this; this.name = name; this.node = node; this.vm = vm; this.nodeType = nodeType; this.update(); Dep.target = null; } Watcher.prototype = { //执行对应的更新函数 update: function () { this.get(); if (this.nodeType == 'text') { this.node.nodeValue = this.value; } if (this.nodeType == 'input') { this.node.value = this.value; } }, // 获取 data 中的属性值 get: function () { this.value = this.vm[this.name]; // 触发相应属性的 get } }复制代码
Watcher()方法接收的参数为vm实例,node节点对象,name传入的节点类型的名称,nodeType节点类型。
首先,将自己赋给了一个全局变量 Dep.target;其次,执行了 update 方法,进而执行了 get 方法,get 的方法读取了 vm 的访问器属性,从而触发了访问器属性的 get 方法,get 方法中将该 watcher 添加到了对应访问器属性的 dep 中;
再次,获取属性的值,然后更新视图。
最后,将 Dep.target 设为空。因为它是全局变量,也是 watcher 与 dep 关联的唯一桥梁,任何时刻都必须保证 Dep.target 只有一个值。
在实例化的时候,我们针对每个属性都添加一个Watcher()订阅者,在observe()的监听属性赋值的时候,将每个属性绑定的订阅者存储在Dep数组中,在set方法触发的时候,调用dep.notify()方法通知Watcher()更新数据,最后实现了视图的更新。
4. 结语
以上就是Vue双向绑定的基本实现原理及代码,当然,这只是基本的实现代码,简单直观的展现给大家看,如果大家想更深入了解的话,推荐大家去阅读这篇文章 。
好啦,以上就是本次的分享,希望对大家理解Vue双向绑定的理解有所帮助,也希望大家有什么不懂或者建议,可以留言互动。