• vue2.x源码理解


    也不知道哪股风潮,钻研源码竟成了深入理解的标配。我只想说一句,说的很对

    准备工作

    1. 从GitHub上面下载vue的源码(https://github.com/vuejs/vue
    2. 了解下Flow,Flow 是 facebook 出品的 JavaScript 静态类型检查工具。Vue.js 的源码利用了 Flow 做了静态类型检查
    3. vue.js 源码目录设计,vue.js的源码都在 src 目录下(vue-devsrc)
      src
      ├── compiler # 编译相关
      ├── core # 核心代码
      ├── platforms # 不同平台的支持
      ├── server # 服务端渲染
      ├── sfc # .vue 文件解析
      ├── shared # 共享代码

      core 目录:包含了 Vue.js 的核心代码,包括内置组件、全局 API 封装,Vue 实例化、观察者、虚拟 DOM、工具函数等等。这里的代码可谓是 Vue.js 的灵魂

      platform目录:Vue.js 是一个跨平台的 MVVM 框架,它可以跑在 web 上,也可以配合 weex 跑在 natvie 客户端上。platform 是 Vue.js 的入口,2 个目录代表 2 个主要入口,分别打包成运行在 web 上和 weex 上的 Vue.js。比如现在比较火热的mpvue框架其实就是在这个目录下面多了一个小程序的运行平台相关内容。

    在这里插入图片描述

    1. vue2.0的生命周期分为4主要个过程:
      4.1 create:创建---实例化Vue(new Vue) 时,会先进行create。
      4.2 mount:挂载---根据el, template, render方法等属性,会生成DOM,并添加到对应位置。
      4.3 update:更新---当数据发生变化后,更新DOM。
      4.4 destory:销毁---销毁时执行

    new Vue()发生了什么

    在vue的生命周期上第一个就是 new Vue() 创建一个vue实例出来,对应到源码在vue-devsrccoreinstanceindex.js

    
    import { initMixin } from './init'
    import { stateMixin } from './state'
    import { renderMixin } from './render'
    import { eventsMixin } from './events'
    import { lifecycleMixin } from './lifecycle'
    import { warn } from '../util/index'
    
    function Vue (options) {
      if (process.env.NODE_ENV !== 'production' &&
        !(this instanceof Vue)
      ) {
        warn('Vue is a constructor and should be called with the `new` keyword')
      }
      this._init(options)
    }
    
    initMixin(Vue)
    stateMixin(Vue)
    eventsMixin(Vue)
    lifecycleMixin(Vue)
    renderMixin(Vue)
    
    export default Vue
    

    可以通过index.js中的代码看到,其实就是一个function,在es5中实现class的方式,在function vue中还加入了if判断,表示vue必须通过new关键字进行实例化。这里有个疑问就是为什么vue中没有使用es6的方式进行定义?通过看下面的方法可以得到解答。

    function vue下定义了许多Mixin这种方法,并且把vue类当作参数传递进去,下面来进入initMixin(Vue)下,来自import { initMixin } from './init',选取了部分代码如下

    
    export function initMixin (Vue: Class<Component>) {
      Vue.prototype._init = function (options?: Object) {
        const vm: Component = this
        // a uid
        vm._uid = uid++
    
        let startTag, endTag
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
          startTag = `vue-perf-start:${vm._uid}`
          endTag = `vue-perf-end:${vm._uid}`
          mark(startTag)
        }
    

    可以看到initMixin方法就是往vue的原型上挂载了一个_init方法,其他的Mixin也是同理,都是往vue的原型上挂载各种方法,而最开始创建vue类时通过es5 function的方式创建也是为了后面可以更加灵活操作,可以将方法写入到各个js文件,不用一次写在一个下面,更加方便代码后期的维护,这个也是选择es5创建的原因。

    当调用new Vue的时候,事实上就调用的Vue原型上的_init方法.

    vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等

    Vue的双向绑定原理

    从index.js入口分析后,越往里发现各个文件之间的引用理不乱剪还乱,于是乎从原来的看源码变成模仿着写雏形,这种方式可能会理解的更加深刻一些,和大家共勉。

    vue中的双向数据是通过数据劫持(Object.defineProperty())结合发布者-订阅者模式来实现的,Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。

    Object.defineProperty()使用

    做个小案例,定义一个svue.js文件,定义一个book对象,并赋值输出

    
    var Book = {
        name: 'vue权威指南'
      };
    console.log(Book.name);  // vue权威指南
    

    得到的结果就是“vue权威指南”,如果想要在执行console.log(book.name)的同时,直接给书名加个书名号怎么做?可以使用Object.defineProperty()来完成,修改后的代码

    
    var Book = {}
    var name = '';
    Object.defineProperty(Book, 'name', {
      set: function (value) {
        name = value;
        console.log('你取了一个书名叫做' + value);
      },
      get: function () {
        return '《' + name + '》'
      }
    })
     
    Book.name = 'vue权威指南';  // 你取了一个书名叫做vue权威指南
    console.log(Book.name);  // 《vue权威指南》
    

    通过Object.defineProperty()对Book对象的name属性的get和set进行了重写操作,当访问name属性时会触发get执行。

    动手模拟写数据双向绑定

    实现mvvm主要包含两个方面,数据变化更新视图,视图变化更新数据。分为3个步骤来做:

    (1) 实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者
    Observer是一个数据监听器,其实现核心方法就是前文所说的Object.defineProperty( )。如果要对所有属性都进行监听的话,那么可以通过递归方法遍历所有属性值,并对其进行Object.defineProperty( )处理。

    对应到源码的目录是:/vue-dev/src/core/observer/index.js

    
    function defineReactive(data, key, val) {
        observe(val); // 递归遍历所有子属性
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function() {
                return val;
            },
            set: function(newVal) {
                val = newVal;
                console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
            }
        });
    }
     
    function observe(data) {
        if (!data || typeof data !== 'object') {
            return;
        }
        Object.keys(data).forEach(function(key) {
            defineReactive(data, key, data[key]);
        });
    };
     
    var library = {
        book1: {
            name: ''
        },
        book2: ''
    };
    observe(library);
    library.book1.name = 'vue权威指南'; // 属性name已经被监听了,现在值为:“vue权威指南”
    library.book2 = '没有此书籍';  // 属性book2已经被监听了,现在值为:“没有此书籍”
    

    因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的,所以要修改下代码

    
    function defineReactive(data, key, val) {
        observe(val); // 递归遍历所有子属性
        var dep = new Dep(); 
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function() {
                if (Dep.target) { //是否需要添加订阅者(Dep.target后类中加入)
                    dep.addSub(Dep.target); // 在这里添加一个订阅者
                }
                return val;
            },
            set: function(newVal) {
                if (val === newVal) {
                    return;
                }
                val = newVal;
                console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
                dep.notify(); // 如果数据变化,通知所有订阅者
            }
        });
    }
    function observe(data) {
        if (!data || typeof data !== 'object') {
            return;
        }
        Object.keys(data).forEach(function(key) {
            defineReactive(data, key, data[key]);
        });
    };
     
    function Dep () {
        this.subs = []; //订阅者的list
    }
    Dep.prototype = {
        addSub: function(sub) {
            this.subs.push(sub);
        },
        notify: function() {
            this.subs.forEach(function(sub) {
                sub.update();
            });
        }
    };
    

    在setter函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数
    (2) 实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图

    
    function Watcher(vm, exp, cb) {
        this.cb = cb;
        this.vm = vm;
        this.exp = exp;
        this.value = this.get();  // 将自己添加到订阅器的操作
    }
     
    Watcher.prototype = {
        update: function() {
            this.run();
        },
        run: function() {
            var value = this.vm.data[this.exp];
            var oldVal = this.value;
            if (value !== oldVal) {
                this.value = value;
                this.cb.call(this.vm, value, oldVal);
            }
        },
        get: function() {
            Dep.target = this;  // 缓存自己
            var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
            Dep.target = null;  // 释放自己
            return value;
        }
    };
    

    简单版的Watcher设计完毕,这时候只要将Observer和Watcher关联起来,就可以实现一个简单的双向绑定数据了,定义index.js

    
    function SelfVue (data, el, exp) {
        this.data = data; //传递进来的{}对象数据
        observe(data);//监听
        el.innerHTML = this.data[exp];  // 初始化模板数据的值 this.data[name]
        //订阅者 function更新会在watcher.js中回调
        new Watcher(this, exp, function (value) {
            el.innerHTML = value;
        });
        return this;
    }
    

    定义index.html引入以上3个js文件进行测试

    
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
    </head>
    <body>
        <h1 id="name">{{name}}</h1>
    </body>
    <script src="observer.js"></script>
    <script src="watcher.js"></script>
    <script src="index.js"></script>
    <script type="text/javascript">
        var ele = document.querySelector('#name');
        var selfVue = new SelfVue({
            name: 'hello world'
        }, ele, 'name');
     
        window.setTimeout(function () {
            console.log('name值改变了');
            selfVue.data.name = '66666666';
        }, 2000);
     
    </script>
    </html>
    

    (3) 实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器
    虽然上面已经实现了一个双向数据绑定的例子,但是整个过程都没有去解析dom节点,而是直接固定某个节点进行替换数据的,所以接下来需要实现一个解析器Compile来做解析和绑定工作

    
    function Compile(el, vm) {
        this.vm = vm;
        this.el = document.querySelector(el);
        this.fragment = null;
        this.init();
    }
    
    Compile.prototype = {
        init: function () {
            if (this.el) {
                this.fragment = this.nodeToFragment(this.el);
                this.compileElement(this.fragment);
                this.el.appendChild(this.fragment);
            } else {
                console.log('Dom元素不存在');
            }
        },
        nodeToFragment: function (el) {
            var fragment = document.createDocumentFragment();
            var child = el.firstChild;
            while (child) {
                // 将Dom元素移入fragment中
                fragment.appendChild(child);
                child = el.firstChild
            }
            return fragment;
        },
        compileElement: function (el) {
            var childNodes = el.childNodes;
            var self = this;
            [].slice.call(childNodes).forEach(function(node) {
                var reg = /{{(.*)}}/;
                var text = node.textContent;
    
                if (self.isTextNode(node) && reg.test(text)) {  // 判断是否是符合这种形式{{}}的指令
                    self.compileText(node, reg.exec(text)[1]);
                }
    
                if (node.childNodes && node.childNodes.length) {
                    self.compileElement(node);  // 继续递归遍历子节点
                }
            });
        },
        compileText: function(node, exp) {
            var self = this;
            var initText = this.vm[exp];
            this.updateText(node, initText);  // 将初始化的数据初始化到视图中
            new Watcher(this.vm, exp, function (value) { // 生成订阅器并绑定更新函数
                self.updateText(node, value);
            });
        },
        updateText: function (node, value) {
            node.textContent = typeof value == 'undefined' ? '' : value;
        },
        isTextNode: function(node) {
            return node.nodeType == 3;
        }
    }
    

    修改index.js

    
    function SelfVue (options) {
        var self = this;
        this.vm = this;
        this.data = options.data;
    
        Object.keys(this.data).forEach(function(key) {
            self.proxyKeys(key);
        });
    
        observe(this.data);
        new Compile(options.el, this.vm);
        return this;
    }
    
    SelfVue.prototype = {
        proxyKeys: function (key) {
            var self = this;
            Object.defineProperty(this, key, {
                enumerable: false,
                configurable: true,
                get: function proxyGetter() {
                    return self.data[key];
                },
                set: function proxySetter(newVal) {
                    self.data[key] = newVal;
                }
            });
        }
    }
    

    修改index.html

    
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
    </head>
    <body>
        <div id="app">
            <h2>{{title}}</h2>
            <h1>{{name}}</h1>
        </div>
    </body>
    <script src="observer.js"></script>
    <script src="watcher.js"></script>
    <script src="compile.js"></script>
    <script src="index.js"></script>
    <script type="text/javascript">
        var selfVue = new SelfVue({
            el: '#app',
            data: {
                title: 'hello world',
                name: ''
            }
        });
     
        window.setTimeout(function () {
            selfVue.title = '你好';
        }, 2000);
     
        window.setTimeout(function () {
            selfVue.name = 'canfoo';
        }, 2500);
     
    </script>
    </html>
    

    现在已经可以解析出{{}}的内容,如果想要支持更多的指令,继续完善compile.js

    
    function Compile(el, vm) {
        this.vm = vm;
        this.el = document.querySelector(el);
        this.fragment = null;
        this.init();
    }
    
    Compile.prototype = {
        init: function () {
            if (this.el) {
                this.fragment = this.nodeToFragment(this.el);
                this.compileElement(this.fragment);
                this.el.appendChild(this.fragment);
            } else {
                console.log('Dom元素不存在');
            }
        },
        nodeToFragment: function (el) {
            var fragment = document.createDocumentFragment();
            var child = el.firstChild;
            while (child) {
                // 将Dom元素移入fragment中
                fragment.appendChild(child);
                child = el.firstChild
            }
            return fragment;
        },
        compileElement: function (el) {
            var childNodes = el.childNodes;
            var self = this;
            [].slice.call(childNodes).forEach(function(node) {
                var reg = /{{(.*)}}/;
                var text = node.textContent;
    
                if (self.isElementNode(node)) {  
                    self.compile(node);
                } else if (self.isTextNode(node) && reg.test(text)) {
                    self.compileText(node, reg.exec(text)[1]);
                }
    
                if (node.childNodes && node.childNodes.length) {
                    self.compileElement(node);
                }
            });
        },
        compile: function(node) {
            var nodeAttrs = node.attributes;
            var self = this;
            Array.prototype.forEach.call(nodeAttrs, function(attr) {
                var attrName = attr.name;
                if (self.isDirective(attrName)) {
                    var exp = attr.value;
                    var dir = attrName.substring(2);
                    if (self.isEventDirective(dir)) {  // 事件指令
                        self.compileEvent(node, self.vm, exp, dir);
                    } else {  // v-model 指令
                        self.compileModel(node, self.vm, exp, dir);
                    }
                    node.removeAttribute(attrName);
                }
            });
        },
        compileText: function(node, exp) {
            var self = this;
            var initText = this.vm[exp];
            this.updateText(node, initText);
            new Watcher(this.vm, exp, function (value) {
                self.updateText(node, value);
            });
        },
        compileEvent: function (node, vm, exp, dir) {
            var eventType = dir.split(':')[1];
            var cb = vm.methods && vm.methods[exp];
    
            if (eventType && cb) {
                node.addEventListener(eventType, cb.bind(vm), false);
            }
        },
        compileModel: function (node, vm, exp, dir) {
            var self = this;
            var val = this.vm[exp];
            this.modelUpdater(node, val);
            new Watcher(this.vm, exp, function (value) {
                self.modelUpdater(node, value);
            });
    
            node.addEventListener('input', function(e) {
                var newValue = e.target.value;
                if (val === newValue) {
                    return;
                }
                self.vm[exp] = newValue;
                val = newValue;
            });
        },
        updateText: function (node, value) {
            node.textContent = typeof value == 'undefined' ? '' : value;
        },
        modelUpdater: function(node, value, oldValue) {
            node.value = typeof value == 'undefined' ? '' : value;
        },
        isDirective: function(attr) {
            return attr.indexOf('v-') == 0;
        },
        isEventDirective: function(dir) {
            return dir.indexOf('on:') === 0;
        },
        isElementNode: function (node) {
            return node.nodeType == 1;
        },
        isTextNode: function(node) {
            return node.nodeType == 3;
        }
    }
    

    修改index.html

    
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
    </head>
    <body>
        <div id="app">
            <h2>{{title}}</h2>
            <input v-model="name">
            <h1>{{name}}</h1>
        </div>
    </body>
    <script src="observer.js"></script>
    <script src="watcher.js"></script>
    <script src="compile.js"></script>
    <script src="index.js"></script>
    <script type="text/javascript">
        var selfVue = new SelfVue({
            el: '#app',
            data: {
                title: 'hello world',
                name: ''
            }
        });
     
    </script>
    </html>
    

    就能看到v-model的效果了

    未完待续

    来源:https://segmentfault.com/a/1190000015846104

  • 相关阅读:
    LeetCode 简单等级
    破解滑动验证码
    python的日志配置
    http/https协议
    Linux通配符和关机命令
    Linux-Shell基础(变量,字符串,数组)
    tf-tensorboard的一些注意事项
    dilated convolution 详解
    论文解读《Understanding the Effective Receptive Field in Deep Convolutional Neural Networks》
    感知域的一些理解
  • 原文地址:https://www.cnblogs.com/qixidi/p/10149711.html
Copyright © 2020-2023  润新知