4
头图

Problem Description

In the interview, the interviewer likes to ask some framework principles in addition to the basic knowledge. For example: 你对vue的数据双向绑定mvvm是如何理解的? Some posts on the Internet may be written in a bit abstract, which is not easy to read and understand quickly. This article uses a simple and easy-to-understand way to explain and implement a simple demo of the two-way binding principle of vue data. I hope it will be helpful to you.

Review the basics first

In order to make it easier for everyone to better understand the code of the two-way data binding below, we'd better review the old knowledge first. If you have solid basic knowledge, you can skip this paragraph directly.

The DOM.children property returns which element child nodes of the DOM element

Code:

 <body>
    <div class="divClass">
        <span>孙悟空</span>
        <h4>猪八戒</h4>
        <input type="text" value="沙和尚">
    </div>
    <script>
        let divBox = document.querySelector('.divClass')
        console.log('元素节点', divBox);
        console.log('元素节点的子节点伪数组', divBox.children);
    </script>
</body>

sample graph:

Note the distinction: DOM.childNodes得到所有的节点 , such as element node, text node, comment node; and, DOM.children只得到所有的元素节点 . Both return a pseudo-array, but the pseudo-array has a length of length, which represents how many nodes there are, and can be traversed in a loop, 遍历的每一项都是一个dom元素标签! but the pseudo-array cannot use the array method

DOM.hasAttribute(key)/getAttribute(key) determines whether the element label has a key attribute and accesses the corresponding value

Code:

 <body>
    <h3 class="styleCss" like="coding" v-bind="fire in the hole">穿越火线</h3>
    <script>
        let h3 = document.querySelector('h3')
        console.log(h3.hasAttribute('v-hello')); // 看看此标签有没有加上v-hello这个属性,没的,故打印:false
        console.log(h3.hasAttribute('like')); // 看看此标签有没有加上like这个属性,有,故打印:true
        console.log(h3.getAttribute('like')); // 访问此标签上加上的这个v-bind属性值是啥,打印:coding
        console.log(h3.hasAttribute('v-bind')); // 看看此标签有没有加上v-bind这个属性,,有的,故打印:true
        console.log(h3.getAttribute('v-bind')); // 访问此标签上加上的这个v-bind属性值是啥,打印:fire in the hole
        console.log(h3.attributes); // 可以看到所有的在标签上绑定的属性名和属性值(key="value"),是一个伪数组
    </script>
</body>

sample graph:

这两个api可以用来看标签上是否绑定了vue的指令,以及看看vue指令值是啥,以便于我们去与data中的相应数据做对应

Difference between DOM.innerHTML and DOM.innerText

Both can modify the text content of the dom. innerHTML is a property that conforms to the W3C standard, so it is the api of the dom used by the mainstream. Although innerText has better compatibility, the mainstream is still innerHTML

Code:

 <body>
    <h3>西游记</h3>
    <button>更改dom内容</button>
    <script>
        let h3 = document.querySelector('h3')
        let btn = document.querySelector('button')
        btn.onclick = () => {
            h3.innerHTML = h3.innerHTML + '6'
        }
    </script>
</body>

sample graph:

DOM.innerHtml这个api可用于更改vue中的差值表达式{{key}}对应的内容值

Data two-way binding finished product renderings

Let's take a look at the renderings of the finished product we want to achieve

demand analysis

  1. The content of the input value in the input box changes, and the page also changes accordingly
  2. Click the button, the input box and the page will change accordingly:
  3. The page change (caused by the input box) triggers the data data change, and finally triggers the page change;
  4. Data data changes (caused by buttons), triggering page changes

Understanding of MVVM

simple to understand

mvvm means mv vm and the corresponding ones are:

  • m is the model data layer (that is, data configuration items such as data, computed, and watch in vue)
  • v is the view view layer (the effect of the view layer is stacked by the dom, so the view layer can be understood as a dom element)
  • vm is the middle layer view_model (vm) layer between the model data layer and the view view layer. It is the core of vue and has powerful functions.

vm可以监听视图层dom的变化 , such as monitoring the value change of the input tag dom, to change the corresponding value of data in the model data layer, vm也可以监听model数据层中的data对应key的value的值的变化, to change the value of the input tag dom.

That is: vm is equivalent to a ferryman, who can ferry people from this bank to the other bank and people from the other bank to this bank

core understanding

所以,MVVM的核心是,所以,MVVM的核心是,所以,MVVM的核心是(重要的事情说三遍:)

  1. 监听页面的DOM的内容值变化,从而通知到data中做对应数据变化(主要是监听表单标签)

    监听表单标签的变化,是使用dom.addEventListener()这个方法
  2. 当data中数据变化以后,再去及时更新页面DOM的内容变化

    监听data中数据的变化,是使用Object.defineProperty()的set方法,自动帮我们监听变化,至于更新dom,就是首先找到要更新哪个dom,如果是普通标签就更新其innerHTML值、如果是表单标签,就更改其value即可

Understanding of Object.defineProperty

Regarding the method of Object.defineProperty, in a word, define responsiveness for the object. There are many information posts on the forum, so I won't go into details here. It is recommended to read the official documentation: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

Regarding this method, we first understand the following cases.

Case requirements

There is an object obj, which has name and age attributes. To make each attribute of this obj responsive, when accessing and modifying, the corresponding printing information must be made.

case code

Copy and paste it and run it, you will understand

 <!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <button id="nameId">修改名字</button>
    <button id="ageId">修改年龄</button>
    <script>
        let obj = {
            name: '孙悟空',
            age: 500,
        }
        for (const key in obj) { // 因为是给对象中每一个属性都添加响应式,所以要遍历对象
            let value = obj[key] // 存一份对应value值,用于访问返回,以及新值修改赋值
            Object.defineProperty(obj, key, { // 给这个obj对象的每一个属性名key都定义响应式
                get() {
                    console.log('访问之(自动触发),访问值为:', value);
                    return value
                },
                set(newVal) {
                    console.log('修改之(自动触发),修改的属性名为:', key, '属性值为:', newVal);
                    value = newVal
                }
            })
        }
        let nameBtn = document.querySelector('#nameId')
        let ageBtn = document.querySelector('#ageId')
        nameBtn.onclick = () => {
            obj.name = obj.name + '^_^ | '
        }
        ageBtn.onclick = () => {
            obj.age = obj.age + 1
        }
        // 这样的话,访问和修改的时候都会触发啦(修改的时候是要先访问找到,再去修改,故打印两次)
    </script>
</body>

</html>

Case renderings

full code

There are a lot of comments written in the code, you should be able to follow the comment steps to read. Just copy and paste for demonstration. Pay attention to the subArr in the code, collect dependencies, the purpose is to see which dom elements need to do subsequent responsive update content

Print the new Vue instance

If the complete code below helps you better understand mvvm, then give us a like to encourage creation ^_^

Complete MVVM code

 <!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #app {
            width: 600px;
            height: 216px;
            background-color: #ccc;
            padding: 24px;
        }

        button {
            cursor: pointer;
        }
    </style>
</head>

<body>

    <!-- view视图层dom,为了便于理解,这里以#app的根元素内部只有一层dom为例(多层需要递归) -->
    <div id="app">
        <input v-model="name" placeholder="请填写名字">
        <span>名字是:</span><span v-bind="name"></span>
        <br>
        <br>
        <input v-model="age" placeholder="请填写年龄">
        <span>年龄是:</span><span v-bind="age"></span>
        <br>
        <br>
        <h3>{{name}}</h3>
        <h3>{{age}}</h3>
        <button id="nameId">更改名字</button>
        <button id="ageId">更改年龄</button>
        <button id="resetId">恢复默认</button>
        <button id="removeId">全部清空</button>
    </div>

    <script>
        // 简单函数封装 之 判断标签内是否包含双差值表达式
        function isIncludesFourKuoHao(str) {
            // 不过这里不是特别严谨。严谨需要使用正则限制,大家明白思路即可
            if (str.length <= 4) { // 得大于4个字符
                return false
            }
            if ( // 且要有双差值表达式,
                str[0] == '{' &
                str[1] == '{' &
                str[str.length - 1] == '}' &
                str[str.length - 2] == '}'
            ) {
                return true
            } else {
                return false
            }
        }

        // 简单函数封装 之 获取双差值表达式之间的变量名
        function getKuoHaoBetweenValue(params) {
            // 这里也不是特别严谨,严谨也需要使用正则,大家明白思路即可
            return params.slice(2, params.length - 2) // {{name}} --> name
        }

        // 这里使用构造函数,使之拥有new的功能。当然也可以使用class类编程
        function Vue(options) {
            /**
             * 第一步,获取根节点dom元素,这一步的作用是有了根节点dom以后,可以通过dom.children获取其所有子节点的dom元素,
             *         便于我们对子节点的dom进行操作,比如给子节点的input标签绑定input事件监听,这样就可以通过dom.value
             *         实时拿到用户在输入框输入的值了
             * */
            this.$el = document.querySelector(options.el);

            /**
             * 第二步,把data中的数据{name:'jack',age:'500'}存一份,因为我们除了修改this.name要是响应式的,同样:
             *        this.$data.name也要是响应式的
            */
            this.$data = options.data;

            /**
             * 第三步,定义一个数组搜集要变化的dom元素,当我们修改data中数据的时候,触发Object.defineProperty()的set方法执行
             *         然后去subArr数组中去寻找,看看是要修改那个dom元素的数据值即可,大家打印一下,就会发现subArr存放的是一个又
             *         一个对象,对象中记录的是 哪一个dom,什么属性名key,以及对应更改innerHTML或value
             * */
            this.subArr = []

            /**
             * 第四步,执行模板编译操作,把data中的数据做页面呈现。这里又分为两部分
             *         4.1 给相应的交互输入类标签绑定事件监听,比如input标签绑定input事件,select标签绑定change事件等。为便于理解
             *             本案例中只以input标签为例说明(当然前提是:加了v-model指令做数据双向绑定才会去操作这一步)
             *         4.2 把v-bind和插值表达式{{}}做内容呈现,即:把model中的对应数据值,并找到对应dom,更改其innerHTML的值为对应数据值
             * */
            this.useDataToCompileRenderPage(); // 使用data中的数据做模板编译并渲染到页面上

            /**
             * 第五步,给m中的数据使用Object.defineProperty做数据劫持,这样的话,访问或者修改对象的属性值时,都可以得知。即:
             *         访问时,不用额外操作。不过修改时,model中的data的值变化了,于此同时,还需同时更新dom,因为m变v也要跟着变
             *         即:dataChangeUpdatePage方法的执行,只要一set更新,我就让dataChangeUpdatePage方法去更新对应的dom值
             *         (因为第四步以后,data中数据是渲染到页面上了,但还需让data中的数据变化,页面也跟着变化,故要做数据劫持)
             * */
            this.definePropertyAllDataKey(); // 数据劫持data中的所有key使之成为响应式的

        }

        // 先把data中的数据,去编译渲染到页面上
        Vue.prototype.useDataToCompileRenderPage = function () {
            let _this = this; // 存一份this实例对象
            let nodes = this.$el.children; // 获取根元素下的所有的子节点dom;值为伪数组,打印结果:[input, span, span, br, br, input, span, span, br, br, button]
            for (let i = 0; i < nodes.length; i++) { // 循环这个子节点dom伪数组,
                let node = nodes[i]; // 所有的标签,一个一个去判断,判断这个标签有没有加上v-model,有没有加上v-bind,有没有差值表达式{{}} ,以这三种情况为例

                // 若dom标签节点上加上了v-model指令
                if (node.hasAttribute('v-model')) {
                    let dataKey = node.getAttribute('v-model');// 去获取v-model绑定的那个属性值,本例中为dataKey的值分别为:name、age
                    node.addEventListener('input', function () { // 以input输入框为例:给标签绑定input输入事件监听,即:<input/>.addEventListener('input',function(){})
                        /** 注意,这里是页面到数据的处理,即v --> vm --> m的流程 */
                        _this.$data[dataKey] = node.value; // 如果是input标签,可以直接通过inputDom.value获取到input标签中用户输入的值
                        _this[dataKey] = node.value; // 上一行是$data更改,即:this.$data.name或age获取dom最新的值、这一行是this.name或age获取最新的值
                    });
                    
                    /** 把model中的数据更新赋值(编译)到页面上 */
                    node['value'] = _this.$data[dataKey]; // inputDom.value = this.$data.name或age 赋值
                    /** 所以,经过这一波操作,成功的把输入框(变化)的值,更改到数据层中了 即:v --> vm --> m */

                    /** 注意这里,就是搜集依赖,可以提取一个方法的,为了便于理解,就不提取了 */
                    _this.subArr.push({
                        nodeLabelDom: node, // 哪个dom标签元素
                        whichAttribute: dataKey, // 哪一个属性name或age
                        valueOrInnerHtml: 'value', // 更改value还是innerHTML
                    })
                }

                // 若dom标签节点上加上了v-bind指令
                if (node.hasAttribute('v-bind')) {
                    /** 如果是v-bind指令,只需要添加watcher即可 * */
                    let dataKey = node.getAttribute('v-bind'); // 去获取v-bind绑定的那个属性值,本例中为dataKey的值分别为:name、age
                    node['innerHTML'] = _this.$data[dataKey]; // normalDom.innerHtml = this.$data.name或age 普通dom显示赋值操作

                    /** 注意这里,就是搜集依赖,可以提取一个方法的,为了便于理解,就不提取了 */
                    _this.subArr.push({
                        nodeLabelDom: node, // 哪个dom标签元素
                        whichAttribute: dataKey, // 哪一个属性name或age
                        valueOrInnerHtml: 'innerHTML', // 更改value还是innerHTML
                    })
                }

                // 如果包含双差值表达式{{}}
                if (isIncludesFourKuoHao(node.textContent)) {
                    let dataKey = getKuoHaoBetweenValue(node.textContent) // 就拿到双差值表达式中间的key,属性名,这里的dataKey分别为:name、age
                    node['innerHTML'] = _this.$data[dataKey]; // 把双差值表达式中的key做一个替换对应值

                    /** 注意这里,就是搜集依赖,可以提取一个方法的,为了便于理解,就不提取了 */
                    _this.subArr.push({
                        nodeLabelDom: node, // 哪个dom标签元素
                        whichAttribute: dataKey, // 哪一个属性name或age
                        valueOrInnerHtml: 'innerHTML', // 更改value还是innerHTML
                    })
                }
            }
        }

        // 再做数据劫持,遍历给data中的每一个数据都劫持,使之,都用于set和get方法
        Vue.prototype.definePropertyAllDataKey = function () {
            let _this = this; // 存一份this以便使用
            for (let key in _this.$data) { // 遍历对象{name:'孙悟空',age: 500}
                let value = _this.$data[key]; // value值为孙悟空、500  key的值自然是name和age
                Object.defineProperty(_this.$data, key, { // 使用defineProperty去添加拦截、劫持(劫持到$data身上)
                    get: function () { // 
                        return value; // 访问key,访问name或者age,就返回对应的值
                    },
                    set: function (newVal) {
                        value = newVal; // 修改key的属性值,修改name或者age的属性值,在做正常操作value = newVal赋值的同时
                        // 每当更新this.$data数据时,如:this.$data.name = 'newVal'就去做对应dom的更新即可
                        _this.dataChangeUpdatePage(key, newVal)
                    }
                })
                Object.defineProperty(_this, key, { // 劫持到自己身上
                    get: function () {
                        return value;
                    },
                    set: function (newVal) {
                        value = newVal;
                        // 每当更新this数据时,如:this.name = 'newVal'就去做对应dom的更新即可
                        _this.dataChangeUpdatePage(key, newVal)
                    }
                })
            }
        }

        // 公共方法,当更新触发的时候,去根据数据做页面渲染
        Vue.prototype.dataChangeUpdatePage = function (key, newVal) {
            let _this = this; // 存一份this实例对象
            // 也要去更新对应dom的内容
            _this.subArr.forEach((item) => {
                if (key == item.whichAttribute) {
                    // 哪个dom的     // innerText或者value      // 赋新值
                    item.nodeLabelDom[item.valueOrInnerHtml] = newVal;
                }
            })
        }

        let vm = new Vue({
            el: '#app', // 指定vue的根元素
            /**
             * model数据层,为了便于理解,这里也是举例data中数据只有一层,多层需要递归
             * */
            data: {
                name: '孙悟空',
                age: 500,
            }
        });
        console.log('vmvm', vm);

        // 更改名字
        let nameBtn = document.querySelector('#nameId')
        nameBtn.onclick = () => {
            vm.name = vm.name + '^' // 直接访问
        }
        // 更改年龄
        let ageBtn = document.querySelector('#ageId')
        ageBtn.onclick = () => {
            vm.$data.age = vm.$data.age * 1 + 1 // 通过$data间接访问
        }
        // 恢复默认的名字和年龄
        let resetBtn = document.querySelector('#resetId')
        resetBtn.onclick = () => {
            vm.$data.name = '孙悟空'
            vm.age = 500
        }
        // 清空名字和年龄
        let removeBtn = document.querySelector('#removeId')
        removeBtn.onclick = () => {
            vm.name = ''
            vm.$data.age = null
        }
    </script>
</body>
</html>
Good memory is not as good as bad writing, record it

水冗水孚
1.1k 声望588 粉丝

每一个不曾起舞的日子,都是对生命的辜负