阅读 481

Vue双向数据绑定原理

转载自CSDN

原理

image

Vue.js 最核心的功能有两个,一是响应式的数据绑定系统,二是组件系统。本文仅探究双向绑定是怎么样实现的。先讲涉及的知识点,再用简化得不能简化得代码实现一个简单的hello world示例。

一、访问器属性

访问器属性是对象中的一种特殊属性,它不能直接在对象中设置,而必须通过defineProperty()方法单独定义。

var obj = {};
Object.defineProperty(obj, "hello", {
  get: function() {
    console.log('get方法被调用了');
  },
  set: function(val) {
    console.log(`set方法被调用了,参数是${val}`)
  }
})

obj.hello; // get方法被调用了
obj.hello = 'abc'; //set方法被调用了,参数是abc
复制代码

get 和 set 方法内部的 this 都指向 obj,这意味着 get 和 set 函数可以操作对象内部的值。另外,访问器属性的会"覆盖"同名的普通属性,因为访问器属性会被优先访问,与其同名的普通属性则会被忽略。

二、极简双向绑定的实现

<!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>
    <input type="text" id="a">
    <span id="b"></span>
    <script>
        const obj = {};
        Object.defineProperty(obj, "hello", {
            set: function(newVal) {
                document.getElementById('a').value = newVal;
                document.getElementById('b').innerHTML = newVal;
            }
        }) // 进行数据绑定

        //增加冒泡事件
        document.addEventListener('keyup', e => {
            obj.hello = e.target.value;
        })
    </script>
</body>
</html>
复制代码

此例实现的效果是:随文本框输入文字的变化,span 中会同步显示相同的文字内容;在js或控制台显式的修改 obj.hello 的值,视图会相应更新。这样就实现了 model => view 以及 view => model 的双向绑定。

以上就是 Vue 实现双向绑定的基本原理。

三、分解任务

上述示例仅仅是为了说明原理。我们最终要实现的是:

<div id="app">
  <input type="text" v-model="text">
  {{ text }}
</div>
复制代码
var vm = new Vue({
  el: '#app',
  data: {
    text: 'hello world'
  }
})
复制代码

首先将该任务分成几个子任务:

  1. 输入框以及文本节点与 data 中的数据绑定
  2. 输入框内容变化时,data 中的数据同步变化。即 view => model 的变化。
  3. data 中的数据变化时,文本节点的内容同步变化。即 model => view 的变化。

要实现任务一,需要对 DOM 进行编译,这里有一个知识点:DocumentFragment。

四、DocumentFragment

DocumentFragment(文档片段)可以看作节点容器,它可以包含多个子节点,当我们将它插入到 DOM 中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。使用 DocumentFragment 处理节点,速度和性能远远优于直接操作 DOM。Vue 进行编译时,就是将挂载目标的所有子节点劫持(真的是劫持,通过 appendChild方法,DOM 中的节点会被自动删除)到 DocumentFragment 中,经过一番处理后,再将 DocumentFragment 整体返回插入挂载目标。

<!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>
    <div id="app">
        <input type="text" id="a">
        <span id="b"></span>
    </div>

    <script>
        const dom = nodeToFragment(document.getElementById('app'));
        console.log(dom)

        function nodeToFragment(node) {
            let flag = document.createDocumentFragment();
            let child;
            while (child = node.firstChild) {
                flag.appendChild(child); //劫持node的所有子节点
            }
            return flag;
        }

        document.getElementById('app').appendChild(dom); // 返回到app中
    </script>
</body>
</html>
复制代码

五、数据绑定初始化

/// 数据初始化绑定
function compile(node, vm) {
    const reg = /\{\{(.*)\}\}/;
    // 节点类型为元素
    if (node.nodeType === 1) {
        const attr = node.attributes;
        // 解析属性
        for (let i = 0; i < attr.length; i++) {
            if (attr[i].nodeName == 'v-model') {
                const name = attr[i].nodeValue; // 获取v-demo绑定的属性名
                node.value = vm.data[name]; // 将data的值赋值给该node
                node.removeAttribute('v-model')
            }
        }
    }
    // 节点类型为text
    if (node.nodeType === 3) {
        if (reg.test(node.nodeValue)) {
            let name = RegExp.$1; // 获取匹配到的字符串
            name = name.trim();
            node.nodeValue = vm.data[name]; // 将data的值赋值给该node
        }
    }
}
复制代码
function nodeToFragment(node, vm) {
    let flag = document.createDocumentFragment();
    let child;
    while (child = node.firstChild) {
        compile(child, vm);
        flag.appendChild(child); // 将子节点接到文档片段中
    }
    return flag;
}

function Vue(options) {
    this.data = options.data;
    const id = options.el;
    const dom = nodeToFragment(document.getElementById(id), this);
    // 编译完成后, 将子节点劫持到文档片段中
    document.getElementById('app').appendChild(dom);
}

const vms = new Vue({
    el: 'app',
    data: {
        text: 'hello world'
    }
})
复制代码
文章分类
前端
文章标签