5分钟教你实现Vue双向绑定

前言

很多人在面试过程中都有问到Vue双向绑定的原理和实现,这是一个老生常谈的面试题了,虽然网上也有很多实现双向绑定的文章,但是我看后觉得对于大多数前端小白来说,不是很容易理解,所以,这篇文章我就用最简单的代码教大家怎么实现一个Vue的双向绑定。

双向绑定的原理

用过Vue框架的都知道,页面在初始化的时候,我们可以把data里的属性渲染到页面上,改动页面上的数据时,data里的属性也会相应的更新,这就是我们所说的双向绑定,所以,简单来说,我们要实现一个双向绑定要实现以下3点操作:

  1. 首先需要在Vue实例化的时候,解析代码中v-modle指令和{{}}指令,然后把data里的属性绑定到相应的指令上,所以我们要实现一个解析器Compile,这是第一点;
  2. 接着我们在改变页面的属性的时候,要知道哪个属性改变了,这时候我们需要用到Object.defineProperty中的gettersetter方法对属性进行劫持,这里我们要实现一个监视器Observer,这是二点;
  3. 我们在知道具体哪个属性改变后,要执行相应的函数,更新视图,这里我们要实现一个消息订阅,在页面初始化的时候订阅每个属性,并且在Object.defineProperty数据劫持的时候接收属性改变通知,更新视图,所以我们要实现一个订阅者Watcher,这是第三点。

1. 实现Compile

首先,我们从最基本的解析指令开始,话不多说,先上代码:

我们在写Vue的时候,用了 v-model{{}}指令,但是页面渲染的时候,我们在浏览器看到的节点是这样的。

我们从上面的图片可以看到,代码里写的指令都消失了,但是data里的属性都正常渲染到页面上了, 原理其实很简单,在Vue实例化的时候,Vue便利循环,扫描和解析每个节点的相关指令,然后再根据对应的指令赋值,最后把相应的指令替换删除,再重新渲染页面。 所以,接下来我们要实现一个解析器Compile,先从解析 v-model{{}}开始。 话不多说,上代码:


"en">


    "UTF-8">
    "viewport" content="width=device-width, initial-scale=1.0">
    "X-UA-Compatible" content="ie=edge">
    MVVMdemo



    
"app"> type="text" v-model="text">
{{text}}
复制代码

上面这段代码就是解析指令的简单方法,我来简单解释一下:

  1. document.createDocumentFragment()
    document.createDocumentFragment() 相当于一个空的容器, 是用来创建一个虚拟的节点对象,在这里我们要做的就是:在遍历节点的同时对相应指令进行解析,解析完一个指令将其添加到createDocumentFragment中,解析完后再重新渲染页面,这样的好处就是减少页面渲染dom的次数,详细内容可参考文档 createDocumentFragment()用法总结
  2. 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()这里就不多说了,具体可以参考文档 defineProperty()使用教程
所以,接下来我们要做的是当我们在输入框输入数据的时候,首先触发 input 事件(或者 keyup、change 事件),在相应的事件处理程序中,我们获取输入框的 value 并赋值给 vm 实例的 text 属性。话不多说,上代码。


"en">


    "UTF-8">
    "viewport" content="width=device-width, initial-scale=1.0">
    "X-UA-Compatible" content="ie=edge">
    MVVMdemo



    
"app"> type="text" v-model="text">
{{text}}
复制代码

我们在页面初始化的时候,通过递归遍历data所有子属性,给每个属性添加一个监视器,在监听到数据变化时候,就会触发defineProperty( )里的set方法,我们可以在控制台输出看到set方法里监听到属性的变化。

从上图我们可以看到,set方法触发了,input里text的属性也变化了, 但是文本节点的内容并没有同步变化,如何让同样绑定到 text 的文本节点也同步变化呢?所以,接下来我们要实现一个之前我们说的订阅者Watcher,在set方法触发时,接受属性改变通知,更新视图。

3. 实现Watcher

很多人看过网上的其他实现MVVM实现的代码,但是都说对Watcher订阅者不是很了解,其实抛开代码,Watcher实现的功能其实很简单,就是当Vue实例化的时候,给每个属性注入一个订阅者Watcher,方便在Object.defineProperty()数据劫持中监听属性的获取(get方法),在Object.defineProperty()监听到数据改变的时候(set方法),通过Watcher通知更新,所以简单来说,Watcher就是起到一个桥梁的作用。我们上面已经通过Object.defineProperty()监听到数据的改变,接下来我们通过实现Watcher 来完成双向绑定的最后一步。


"en">


    "UTF-8">
    "viewport" content="width=device-width, initial-scale=1.0">
    "X-UA-Compatible" content="ie=edge">
    MVVMdemo



    
"app"> type="text" v-model="text">
{{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的双向绑定原理及实现 。

好啦,以上就是本次的分享,希望对大家理解Vue双向绑定的理解有所帮助,也希望大家有什么不懂或者建议,可以留言互动。

转载于:https://juejin.im/post/5c9832af5188252db5635082

你可能感兴趣的:(5分钟教你实现Vue双向绑定)