Vue源码学习之双向数据绑定原理

前言

 对于Vue的双向数据绑定原理相信大家在面试时,一定是常考常问,那么我们今天就讲解一下Vue双向数据绑定的原理~

代码解析

 那么接下来我们去模拟一下Vue初始化时的代码,来进行分析。

import Compile from "./Compile.js";
import observe from './observe.js';
import Watcher from './Watcher.js';

export default class Vue {
    
    
    constructor(options) {
    
    
        // 把参数options对象存为$options
        this.$options = options || {
    
    };
        // 数据
        this._data = options.data || undefined;
        // 将data数据变为响应式的
        observe(this._data);
        // 默认数据要变为响应式的,这里就是生命周期
        this._initData();
        // 调用默认的watch
        this._initWatch();
        // 模板编译
        new Compile(options.el, this);
    }

    _initData() {
    
    
        var self = this;
        Object.keys(this._data).forEach(key => {
    
    
            Object.defineProperty(self, key, {
    
    
                get: () => {
    
    
                    return self._data[key];
                },
                set: (newVal) => {
    
    
                    self._data[key] = newVal;
                }
            });
        });
    }

    _initWatch() {
    
    
        var self = this;
        var watch = this.$options.watch;
        Object.keys(watch).forEach(key => {
    
    
            new Watcher(self, key, watch[key]);
        });
    }

};

 通过代码我们可以分析当初始化Vue的时候,我们使用observe来递归data属性,进行将data属性变为响应式的,其中在递归data属性时,会通过defineReactive来对每个属性都添加对应的数据劫持。接下来会进入Compile阶段,我们对Compile阶段进行分析。
 代码如下:

import Watcher from './Watcher.js';

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) {
    
    
        // 创建了虚拟的节点对象,只要把节点放到了Fragment节点中,那么文档中真实的节点就消失了
        var fragment = document.createDocumentFragment();

        var child;
        // 让所有DOM节点,都进入fragment
        while (child = el.firstChild) {
    
    
            fragment.appendChild(child);
        }
        return fragment;
    }
    compile(el) {
    
    
        // console.log(el);
        // 得到子元素
        var childNodes = el.childNodes;
        var self = this;

        // {
    
    {}}模板匹配正则表达式
        var reg = /\{\{(.*)\}\}/;

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

    compileElement(node) {
    
    
        // console.log(node);
        // 这里的方便之处在于不是将HTML结构看做字符串,而是真正的属性列表
        var nodeAttrs = node.attributes;
        var self = this;

        // 类数组对象变为数组
        [].slice.call(nodeAttrs).forEach(attr => {
    
    
            // 这里就分析指令
            // 属性名
            var attrName = attr.name;
            // 属性值
            var value = attr.value;
            // 指令都是v-开头的
            var dir = attrName.substring(2);

            // 看看是不是指令
            if (attrName.indexOf('v-') == 0) {
    
    
                // v-开头的就是指令
                if (dir == 'model') {
    
    
                    // console.log('发现了model指令', value);
                    // 这里也添加了订阅者,当Model层的数据发生改变后,会触发update,
                    // 从而在Complie中触发对应的回调函数,进行更新
                    new Watcher(self.$vue, value, value => {
    
    
                        node.value = value;
                    });

                    // 将Model层的数据赋值给value
                    var v = self.getVueVal(self.$vue, value);
                    node.value = v;

                    // 添加监听
                    node.addEventListener('input', e => {
    
    
                        var newVal = e.target.value;
                        self.setVueVal(self.$vue, value, newVal);
                        v = newVal; 
                    });

                } else if (dir == 'if') {
    
    
                    // console.log('发现了if指令', value);
                }
            }
        });
    }

    compileText(node, name) {
    
    
        // console.log('AA', name);
        // console.log('BB', this.getVueVal(this.$vue, name));
        node.textContent = this.getVueVal(this.$vue, name);
        new Watcher(this.$vue, name, value => {
    
    
            node.textContent = value;
        });
    }

    getVueVal(vue, exp) {
    
    
        var val = vue;
        // 比如过去"a.b.c.d",此时我们就可以通过.来对其进行分割
        exp = exp.split('.');
        // 在这一步中val可以赋值为对象属性为d的值
        exp.forEach(k => {
    
    
            val = val[k];
        });

        return val;
    }

    setVueVal(vue, exp, value) {
    
    
        var val = vue;
        // 比如过去"a.b.c.d",此时我们就可以通过.来对其进行分割
        exp = exp.split('.');
        exp.forEach((k, i) => {
    
    
            if (i < exp.length - 1) {
    
    
                val = val[k];
            } else {
    
    
                // 当获取到对象的d属性时,直接将其赋值给value属性
                val[k] = value;
            }
        });

    }
}

 通过这段代码进行分析,在compile阶段时,进行遍历子节点通过nodeType去判断当前节点是元素节点还是文本节点。
 当为元素节点的时候我们就去获取对应的attributes属性值,去判断是否有v-model或者v-if等等属性值。如果是v-model,首先先把Model层的数据赋值给v-model所对应的属性,然后给予这个属性添加一个订阅者,当数据发生变化时,进行更新这个textContent,最后添加一个Input监听,用于监听用户输入,当用户输入时,我们更新Model层的数据,此时因为Model层的数据发生了变化,则会调用订阅者对应的回调,使其View层也会发生改变。
 如果是文本节点的话我们就通过正则表达式去匹配{ {}}之间的属性值,然后从Model层中取出属性所对应的内容值替换给textContent,在给当前的这个属性添加一个订阅者对象,其对应的回调是当数据发生变化时,进行更新textContent。

总结

  1. 首先会向Model层的数据对象进行递归添加Observe实例,并在defineReactive函数中通过Object.defineProperty向对应数据进行数据劫持,当获取或赋值时就会触发getter和setter。
  2. 在Compile阶段解析模板一共有两种情况,一种是使用v-model来对value值进行绑定,一种是被大括号包裹的文本绑定
  3. 当遇到了元素节点,并且属性值包含v-model的话,我们就从Model层去获取v-model所对应属性的值,并赋值给value值,并给节点添加监听input监听事件,当View中元素的数据发生变化的时候,也就是触发input事件的时候去,去将Model层的数据进行更新
  4. 当遇到文本节点的时候,获取Model层对应的属性来替换这个文本节点,并给予这个文本节点添加一个订阅者对象,将这个订阅者对象放入Dep对象的订阅者列表中,当Model层的数据发生变化也就是触发了setter函数,就会调用当前Observer中dep实例对象的notify方法,去通知订阅者列表中所有的订阅者进行调用update方法,触发Compile中绑定的回调进行更新视图。

猜你喜欢

转载自blog.csdn.net/liu19721018/article/details/125532030