vue - 原理

vue 原理

大概思路

vue的数据驱动主要实现建立在三个对象上Dep、Watcher、Compiler

Dep 主要负责依赖的收集
Watcher 主要负责Dep和Compiler之间的联系
Compiler 可以理解为 virtual dom + patch 也就是负责视图层的渲染

基本原理

1、建立虚拟DOM Tree,通过document.createDocumentFragment(),遍历指定根节点内部节点,根据{{ prop }}、v-model等规则进行compile;
2、通过Object.defineProperty()进行数据变化拦截;
3、截取到的数据变化,通过发布者-订阅者模式,触发Watcher,从而改变虚拟DOM中的具体数据;
4、通过改变虚拟DOM元素值,从而改变最后渲染dom树的值,完成双向绑定

完成数据的双向绑定在于Object.defineProperty()

双向绑定的实现

简易双绑

原理 Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
语法:Object.defineProperty(obj, prop, descriptor)

var obj = {};
Object.defineProperty(obj,'hello',{
  get:function(){
    //我们在这里拦截到了数据
    console.log("get方法被调用");
  },
  set:function(newValue){
    //改变数据的值,拦截下来额
    console.log("set方法被调用");
  }
});
obj.hello//输出为“get方法被调用”,输出了值。
obj.hello = 'new Hello';//输出为set方法被调用,修改了新值

通过以上方法可以看出,获取对象属性值触发get、设置对象属性值触发set,因此我们可以想象到数据模型对象的属性设置和读取可以驱动view层的数据变化,view的数据变化传递给数据模型对象,在set里面可以做很多事情。

数据的双向绑定

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

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>数据的双向绑定</title>
</head>

<body>
    <input class="inp-text" type="text">
    <div class="text-box"></div>
    <button class="btn">getInputValue</button>
    <script>
        var obj = {
            name: 'init'
        };

        console.log(JSON.stringify(obj)) // {"name":"init"}

        Object.defineProperty(obj, 'name', {
            set: function (newValue) {
                console.log('触发setter:',newValue);
                document.querySelector('.text-box').innerHTML = newValue;
                document.querySelector('.inp-text').value = newValue;
            },
            get: function () {
                console.log('触发getter');
            }
        });

        console.log(JSON.stringify(obj)) // {}

        document.querySelector('.inp-text').addEventListener('keyup', function (e) {
            obj.name = e.target.value;
        }, false);

        document.querySelector('.btn').addEventListener('click', (function (e) {
            // ???: // console.log(obj.name) // undefined
            console.log(obj)
        }), false);
    </script>
</body>

</html>

虚拟DOM树

创建虚拟DOM

var frag = document.createDocumentFragment();

view层的{{msg}}和v-model的编译规则如下

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

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>虚拟DOM树</title>
</head>

<body>
    <!-- view层做了多层嵌套,这样测试更多出现错误的可能性。 -->
    <div id="container">
        {{ msg }}<br>
        <input class="inp-text" type="text" v-model="inpText">
        <div class="text-box">
            <p class="show-text">{{ msg }}</p>
        </div>
    </div>
    <script>
        var container = document.getElementById('container');
        //这里我们把vue实例中的data提取出来,更加直观
        var data = {
            msg: 'Hello world!',
            inpText: 'Input text'
        };
        var fragment = virtualDom(container, data);
        container.appendChild(fragment);

        //虚拟dom创建方法
        function virtualDom(node, data) {
            let frag = document.createDocumentFragment();
            let child;
            console.log(node)
            // 遍历dom节点
            while (child = node.firstChild) {
                compile(child, data);
                frag.appendChild(child);
            }
            console.log(frag)
            return frag;
        }

        //编译规则
        function compile(node, data) {
            let reg = /\{\{(.*)\}\}/g;
            if (node.nodeType === 1) { // 标签
                let attr = node.attributes;
                for (let i = 0, len = attr.length; i < len; i++) {
                    // console.log(attr[i].nodeName, attr[i].nodeValue);
                    if (attr[i].nodeName === 'v-model') {
                        let name = attr[i].nodeValue;
                        node.value = data[name];
                    }
                }
                if (node.hasChildNodes()) {
                    node.childNodes.forEach((item) => {
                        compile(item, data); // 递归
                    });
                }
            }
            if (node.nodeType === 3) { // 文本节点
                if (reg.test(node.nodeValue)) {
                    let name = RegExp.$1;
                    name = name.trim();
                    node.nodeValue = data[name];
                }
            }
        }
    </script>
</body>

</html>

解释:
1、通过virtualDom创建虚拟节点,将目标盒子内所有子节点添加到其内部,注意这里只是子节点;
2、子节点通过compile进行编译,a:如果节点为元素,其nodeType = 1,b:如果节点为文本,其nodeType = 3,具体可以查看详情
3、如果第二步子节点仍有子节点,通过hasChildNodes()来确认,如果有递归调用compile方法。

else

浏览器运行时会把template转换为render函数,webpack则不需要,因为vue-loader已经转换完成
get数据收集是在render函数中执行的,每个computed函数都会生成一个watcher和data里的数据绑定,data变化后watcher执行,(所以才有了缓存),每个组件都会生成一个渲染watcher,里面主要是做微任务部分,总之同步任务做数据收集,微任务做diff及dom渲染。

参考链接

Vue基本原理
Vue 原理解析

posted @ 2021-11-03 13:46  zc-lee  阅读(401)  评论(0编辑  收藏  举报