likes
comments
collection
share

简略实现Vue中v-model指令的双向绑定功能

作者站长头像
站长
· 阅读数 64

前言

创建Vue类实现数据初始化操作

Vue类中主要实现的功能如下:

  1. 将Vue接收到的对象数据复制一份存储到Vue类的实例 $options 身上
  2. 将Vue接收的对象数据存储到Vue类实例的 _data
  3. 调用 observe 方法,将 _data 数据变成响应式的
  4. _data 上的数据循环遍历一份放到 Vue的实例上
  5. 初始化 watch,将设置的watch监控项数据放到 Watcher 里面进行监控
import {observe} from "../vue2ResponsivePrinciple/observe";
import Watcher from "../vue2ResponsivePrinciple/Watcher";
import Compile from "./Compile";
export default class Vue{
    constructor(options){
        //把参数options对象存为$options;
        this.$options = options || {};
        //存储数据
        this._data = options.data || undefined;
        //将数据变成响应式的
        observe(this._data);
        // 默认数据变成响应式的,将_data上的数据复制一份到this上。
        this._initData()
        //调用默认的Watcher
        this._initWatch()
        //进行模板编译
        new Compile(options.el,this);
    }
    _initData(){
        let self = this;
        Object.keys(this._data).forEach(key=>{
            Object.defineProperty(self,key,{
                get:()=>{
                    return self._data[key];
                },
                set:(newValue)=>{
                    self._data[key] = newValue;
                }
            })
        })
    }

    _initWatch(){
        let self = this;
        //获取参数里面的watch项
        let watch = this.$options.watch || {};
        //设置watch项里面的监听
        Object.keys(watch).forEach(key=>{
            new Watcher(self,key,watch[key]);
        })
    }
}

创建编译类Compile

Compile类中实现的功能如下

  1. 使用 **fragment **节点替代虚拟节点,将获取到的dom树节点放到fragment节点上。
  2. 编译dom树,循环遍历节点,根据节点类型不同执行不同的操作。这里主要用到了元素节点和文本节点。
  3. 上树,将操作好的 fragment 节点放到 el 上。
import Watcher from "../vue2ResponsivePrinciple/Watcher";
export default class Compile{
    constructor(el,vue) {
        //vue实例
        this.$vue = vue;
        //挂载点
        this.$el = document.querySelector(el);
        if(this.$el){
            // 调用函数,让节点变为fragment,类似于mustache中的tokens。实际上用的是AST,这里就是轻量级的,fragment
            let $fragment = this.node2Fragment(this.$el);
            //编译
            this.compile($fragment);
            //替换好的内容要上树
            this.$el.appendChild($fragment);
        }
    }

    node2Fragment(el){
        /**
         * 创造一个DocumentFragment类型节点,可以把它当作虚拟节点,消耗小
         * 主要作用:充当其他要被添加到文档的节点的仓库,也就是充当初始父节点
         * 其parentNode值为空
         * @type {DocumentFragment}
         */
        let fragment = document.createDocumentFragment();
        let child;
        //获取文档节点中的第一个节点,并将其赋值给child
        while(child = el.firstChild){
            fragment.appendChild(child);
        }
        return fragment;
    }

    //编译
    compile(el){
        ...
    }
}

对于文本节点的操作

  1. 获取文本节点上的文本,匹配其是否存在 双大括号( {{}} )语法,
  2. 通过 getVueVal 方法读取双大括号里面的值,
  3. 将双大括号的值作为监控项通过 Watcher 监控(收集依赖)
  4. 当监控项值发生变化时(触发依赖),通过回调函数将新值赋值到文本节点上

到这里就实现了data中的数据值发生变化时同步更新界面上引用的数据

import Watcher from "../vue2ResponsivePrinciple/Watcher";
export default class Compile{
    constructor(el,vue) {
        ...
    }

    node2Fragment(el){
        ...
    }

    //编译
    compile(el){
        let childNodes = el.childNodes;
        let self = this;
        //捕获{{}}文本数据
        let reg = /\{\{(.*)\}\}/;
        childNodes.forEach(node=>{
            //获取节点的文本
            let text = node.textContent;
            //nodeType值为1,代表元素节点
            if(node.nodeType == 1){
                self.compileElement(node);
            }else if(node.nodeType == 3 && reg.test(text)){
                //nodeType值为3,代表文本节点
                let name = text.match(reg)[1];
                self.compileText(node,name);
            }
        })
    }

    compileElement(node){
        ...
    }

    compileText(node,name){
        //之前已经把data里面的数据循环遍历放到vue上了,所以可以传入vue
        node.textContent = this.getVueVal(this.$vue,name);
        //设置监控响应
        new Watcher(this.$vue,name,(newValue)=>{
            node.textContent = newValue;
        })
    }

    //获取使用的值a.b.n
    getVueVal(vue,exp){
        let val = vue;
        exp = exp.split(".");
        exp.forEach(key=>{
            val = val[key];
        })
        return val;
    }
}

对于元素节点的操作

  1. 获取元素节点的属性,如class、id、style、type等等,也包含自定义的属性v-model、v-if等等。获取到的属性集是一个类数组。
  2. 循环遍历属性集上的属性,找出是否含有v-model属性。
  3. 获取到v-model属性上的值。如下,v-model的值就为a.b.n
<input type="text" v-model="a.b.n"/>
  1. 使用 Watcher 对 a.b.n 进行监控,在回调函数中将新值赋值给元素节点的值。到这里就实现了Vue数据到界面视图的同步,即Vue中的数据进行变动时会将新值更新到界面上。
  2. 使用 getVueVal获取a.b.n 对应的值,将值赋值给元素节点的值,实现绑定值的初始化
  3. 给元素节点设置addEventListener监听事件,触发事件时在回调函数中使用setVueVal方法将获取的新值赋值给监控项a.b.n对应的值。到这就实现了界面视图到Vue数据的同步,即界面的数据变动时,将变动值更新到Vue数据上。

完整代码:

import Watcher from "../vue2ResponsivePrinciple/Watcher";
export default class Compile{
    constructor(el,vue) {
        //vue实例
        this.$vue = vue;
        //挂载点
        this.$el = document.querySelector(el);
        if(this.$el){
            // 调用函数,让节点变为fragment,类似于mustache中的tokens。实际上用的是AST,这里就是轻量级的,fragment
            let $fragment = this.node2Fragment(this.$el);
            //编译
            this.compile($fragment);
            //替换好的内容要上树
            this.$el.appendChild($fragment);
        }
    }

    node2Fragment(el){
        /**
         * 创造一个DocumentFragment类型节点,可以把它当作虚拟节点,消耗小
         * 主要作用:充当其他要被添加到文档的节点的仓库,也就是充当初始父节点
         * 其parentNode值为空
         * @type {DocumentFragment}
         */
        let fragment = document.createDocumentFragment();
        let child;
        //获取文档节点中的第一个节点,并将其赋值给child
        while(child = el.firstChild){
            fragment.appendChild(child);
        }
        return fragment;
    }

    //编译
    compile(el){
        let childNodes = el.childNodes;
        let self = this;

        //捕获{{}}文本数据
        let reg = /\{\{(.*)\}\}/;

        childNodes.forEach(node=>{
            //获取节点的文本
            let text = node.textContent;
            //nodeType值为1,代表元素节点
            if(node.nodeType == 1){
                self.compileElement(node);
            }else if(node.nodeType == 3 && reg.test(text)){
                //nodeType值为3,代表文本节点
                let name = text.match(reg)[1];
                self.compileText(node,name);
            }
        })
    }

    compileElement(node){
        //获取元素节点的属性,比如class,id等等。
        let nodeAttrs = node.attributes;
        //获取的nodeAttrs是一个类数组对象,将其转化为数组对象
        let nodeAttrsArray = [...nodeAttrs];
        let self = this;
        nodeAttrsArray.forEach(attr=>{
            //在这里分析指令
            let attrName = attr.name;
            let value = attr.value;
            //指令都是v-开头的,取字符串2位后字符
            let dir = attrName.substring(2);
            //判断是否为指令
            if(attrName.indexOf('v-') == 0){
                //双向绑定
                if(dir == 'model'){
                    // console.log('捕捉到v-model指令',node);
                    //将实际值放入v-model绑定的值
                    //此时vue中的数据改变会影响界面的数据,数据从vue流向界面。
                    new Watcher(self.$vue,value,value=>{
                        node.value = value;
                    });
                    //读取绑定的值
                    let v = self.getVueVal(self.$vue,value);
                    node.value = v;

                    //添加界面数据变动影响vue里面的数据,通过给元素添加监听事件实现。
                    node.addEventListener('input',e=>{
                        let newVal = e.target.value;
                        self.setVueVal(self.$vue,value,newVal);
                        v = newVal;
                    })
                }
            }
        })
    }

    compileText(node,name){
        //之前已经把data里面的数据循环遍历放到vue上了,所以可以传入vue
        node.textContent = this.getVueVal(this.$vue,name);
        //设置监控响应
        new Watcher(this.$vue,name,(newValue)=>{
            node.textContent = newValue;
        })
    }

    //获取使用的值a.b.n
    getVueVal(vue,exp){
        let val = vue;
        exp = exp.split(".");
        exp.forEach(key=>{
            val = val[key];
        })
        return val;
    }

    //修改值
    setVueVal(vue,exp,value){
        let val = vue;
        exp = exp.split(".");
        exp.forEach((key,i)=>{
            //获取最底层的值修改
            if(i< exp.length - 1){
                val = val[key]
            }else{
                val[key] = value;
            }
        })
    }
}

index.js中引入测试

通过webpack打包后的js文件中的方法和变量都会变成局部的,外部无法直接访问,因此可以将其添加到window上,作为全局属性或方法,方便外部访问。

import Vue from './Vue'
window.Vue = Vue;

index.html主页测试

这个index.html是初始模板,通过webpack打包后会使打包后的bundle.js文件自动的插入html模板文件中。 使用setTimeout是为了在JS文件加载解析后在进行实例化Vue,防止实例Vue类的时候其还未加载出来。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div id="app">
        {{a.b.n}}
        <h2 id="h2" >--------------------------------------</h2>
        <input type="text" v-model="a.b.n"/>
    </div>
</body>
<script>
    let vm = null;
    setTimeout(()=>{
        vm = new Vue({
            el:"#app",
            data:{
                a:{
                    b:{
                        n:10
                    }
                },
                b:false,
                c:55
            },
            watch:{
                b:function(newValue,oldValue){
                    console.log(newValue,oldValue)
                }
            }
        });
    },500);
    console.log(vm);
</script>
</html>

测试结果

简略实现Vue中v-model指令的双向绑定功能

输入框的数据和文本的数据保持同步,一个更新另一个也同步更新。

小结

v-model如何实现数据双向绑定?

  1. 先通过observe实现对Vue数据的监听。
  2. 通过Watcher监控v-model绑定项,并在其回调函数里面将新值更新到界面。实现了从Vue中的数据到界面的流动。
  3. 通过给监控的元素节点添加addEventListener事件来监听元素的改变,并在其回调函数里将界面最新值更新到Vue数据中。从而实现了数据从界面流向Vue数据。
转载自:https://juejin.cn/post/7248898395883585592
评论
请登录