透過迷你vue庫,瞭解vue背後思想

更多詳情請參考 http://blog.zhangbing.club/Ja...vue

/**
 * the super tiny vue.js.
 簡介:一個迷你vue庫,雖然小但功能全面,能夠做爲想了解vue背後思想以及想學習vue源碼而又不知如何入手的入門學習資料。
 
 特性:
 * 數據響應式更新
 * 指令模板
 * MVVM
 * 輕量級
 
 ## 功能解讀
 <templete>
    <div id='app'>
        <div>
            <input v-model='counter' />
            <button v-on-click='add'>add</button>
            <p v-text='counter'></p>
        </div>
    </div>
 </templete>
 <script>
var vm = new Vue({
        id: 'counter',
        data: {
            counter: 1
        },
        methods: {
            add: function () {
                this.counter += 1;
            }
        }
    })
 </script>
 
 如上爲一段模板以及js腳本,咱們所要實現的目標就是將 vm 實例與id爲app的DOM節點關聯起來,當更改vm data 的counter屬性的時候,
 input的值和p標籤的文本會響應式的改變,method中的add方法則和button的click事件綁定。
 簡單的說就是, 當點擊button按鈕的時候,觸發button的點擊事件回調函數add,在add方法中使counter加1,counter變化後模板中的input
 和p標籤會自動更新。vm與模板之間是如何關聯的則是經過 v-model、v-on-click、v-text這樣的指令聲明的。   
### 實現思路詳解
 * 查找含指令的節點
 * 對查找所得的節點進行指令解析、指令所對應的實現與節點綁定、 節點指令值所對應的data屬性與前一步關聯的指令實現綁定、data屬性值經過setter通知關聯的指令進行更新操做
 * 含指令的每個節點單獨執行第二步
 * 綁定操做完成後,初始化vm實例屬性值
#### 指令節點查找
 首先來看第一步,含指令節點的查找,由於指令聲明是以屬性的形式,因此能夠經過屬性選擇器來進行查找,以下所示:
 `<input v-model='counter' type='text' />`
 則可經過 querySelectorAll('[v-model]') 查找便可。
    root = this.$el = document.getElementById(opts.el),
    els  = this.$els = root.querySelectorAll(getDirSelectors(Directives))
 root對於根節點,els對應於模板內含指令的節點。
 #### 指令解析,綁定
 
 * 1.指令解析
 一樣以`<input v-model='counter' type='text' />`爲例,解析即獲得
 var directive = {
    name: 'v-model',
    value: 'counter'
 }
 name對應指令名,value對應指令值。
 * 2.指令對應實現與當前節點的綁定(bindDirective)
 指令實現可簡單分爲函數或是包含update函數的對象,以下即是`v-text`指令的實現代碼:
 text: function (el, value) {
        el.textContent = value || '';
    }

 指令與節點的綁定即將該函數與節點綁定起來,即該函數負責該節點的更新操做,`v-text`的功能是更新文本值,因此如上所示
 更改節點的textContent屬性值。
 * 3. 響應式數據與節點的綁定(bindAccessors)
    響應式數據這裏拆分爲 data 和 methods 對象,分別用來存儲數據值和方法。
    
    var vm = new Vue({
        id: 'counter',
        data: {
            counter: 1
        },
        methods: {
            add: function () {
                this.counter += 1;
            }
        }
    })
  
    咱們上面解析獲得 v-model 對於的指令值爲 counter,因此這裏將data中的counter與當前節點綁定。
 
 經過二、3兩步實現了類型與 textDirective->el<-data.counter 的關聯,當data.counter發生set(具體查看defineProperty set 用法)操做時,
 data.counter得知本身被改變了,因此通知el元素須要進行更新操做,el則使用與其關聯的指令(textDirective)對自身進行更新操做,從而實現了數據的
 響應式。
    * textDirective
    * el
    * data.counter
 這三個是綁定的主體,數據發生更改,通知節點須要更新,節點經過指令更新本身。
 * 4.其它相關操做
 */



var prefix = 'v';
 /**
  * Directives
  */

var Directives = {
    /**
     * 對應於 v-text 指令
     */
    text: function (el, value) {
        el.textContent = value || '';
    },
    show: function (el, value) {
        el.style.display = value ? '' : 'none';
    },
    /**
     * 對應於 v-model 指令
     */
    model: function (el, value, dirAgr, dir, vm, key) {
        let eventName = 'keyup';
        el.value = value || '';

        /**
         * 事件綁定控制
         */
        if (el.handlers && el.handlers[eventName]) {
            el.removeEventListener(eventName, el.handlers[eventName]);
        } else {
            el.handlers = {};
        }

        el.handlers[eventName] = function (e) {
            vm[key] = e.target.value;
        }

        el.addEventListener(eventName, el.handlers[eventName]);
    },
    on: {
        update: function (el, handler, eventName, directive) {
            if (!directive.handlers) {
                directive.handlers = {}
            }

            var handlers = directive.handlers;

            if (handlers[eventName]) {
                //綁定新的事件前移除原綁定的事件函數
                el.removeEventListener(eventName, handlers[eventName]);
            }
            //綁定新的事件函數
            if (handler) {
                handler = handler.bind(el);
                el.addEventListener(eventName, handler);
                handlers[eventName] = handler;
            }
        }
    }
}


/**
 * MiniVue 
 */
function TinyVue (opts) {
    /**
     * root/this.$el: 根節點
     * els: 指令節點
     * bindings: 指令與data關聯的橋樑
     */
    var self = this,
        root = this.$el = document.getElementById(opts.el),
        els  = this.$els = root.querySelectorAll(getDirSelectors(Directives)),
        bindings = {};
    this._bindings = bindings;

    /**
     * 指令處理
     */
    [].forEach.call(els, processNode);
    processNode(root);

    /**
     * vm響應式數據初始化
     */

    let _data = extend(opts.data, opts.methods);
    for (var key in bindings) {
        if (bindings.hasOwnProperty(key)) {
            self[key] = _data[key];
        }
    }

    function processNode (el) {
        getAttributes(el.attributes).forEach(function (attr) {
            var directive = parseDirective(attr);
            if (directive) {
                bindDirective(self, el, bindings, directive);
            }
        })
    }

    /**
     * ready
     */
    if (opts.ready && typeof opts.ready == 'function') {
        this.ready = opts.ready;
        this.ready();
    }
}

/**************************************************************
 * @privete
 * helper methods
 */

/**
 * 獲取節點屬性
 * 'v-text'='counter' => {name: v-text, value: 'counter'}
 */
function getAttributes (attributes) {
    return [].map.call(attributes, function (attr) {
        return {
            name: attr.name,
            value: attr.value
        }
    })
}

/**
 * 返回指令選擇器,便於指令節點的查找
 */
function getDirSelectors (directives) {
    /**
     * 支持的事件指令
     */
    let eventArr = ['click', 'change', 'blur']; 


    return Object.keys(directives).map(function (directive) {
        /**
         * text => 'v-text'
         */
        return '[' + prefix + '-' + directive + ']';
    }).join() + ',' + eventArr.map(function (eventName) {
        return '[' + prefix + '-on-' + eventName + ']';
    }).join();
}

/**
 * 節點指令綁定
 */
function bindDirective (vm, el, bindings, directive) {
    //從節點屬性中移除指令聲明
    el.removeAttribute(directive.attr.name);
    
    /**
     * v-text='counter'
     * v-model='counter'
     * data = { 
            counter: 1 
        } 
     * 這裏的 counter 即指令的 key
     */
    var key = directive.key,
        binding = bindings[key];

    if (!binding) {
        /**
         * value 即 counter 對應的值
         * directives 即 key 所綁定的相關指令
         如:
           bindings['counter'] = {
                value: 1,
                directives: [textDirective, modelDirective]
             }
         */
        bindings[key] = binding = {
            value: '',
            directives: []
        }
    }
    directive.el = el;
    binding.directives.push(directive);

    //避免重複定義
    if (!vm.hasOwnProperty(key)) {
        /**
         * get/set 操做綁定
         */
        bindAccessors(vm, key, binding);
    }
}

/**
 * get/set 綁定指令更新操做
 */
function bindAccessors (vm, key, binding) {
    Object.defineProperty(vm, key, {
        get: function () {
            return binding.value;
        },
        set: function (value) {
            binding.value = value;
            binding.directives.forEach(function (directive) {
                directive.update(
                    directive.el,
                    value,
                    directive.argument,
                    directive,
                    vm,
                    key
                )
            })
        }
    })
}

function parseDirective (attr) {
    if (attr.name.indexOf(prefix) === -1) return ;

    /**
     * 指令解析
       v-on-click='onClick'
       這裏的指令名稱爲 'on', 'click'爲指令的參數,onClick 爲key
     */

    //移除 'v-' 前綴, 提取指令名稱、指令參數
    var directiveStr = attr.name.slice(prefix.length + 1),
        argIndex = directiveStr.indexOf('-'),
        directiveName = argIndex === -1
            ? directiveStr
            : directiveStr.slice(0, argIndex),
        directiveDef = Directives[directiveName],
        arg = argIndex === -1
            ? null
            : directiveStr.slice(argIndex + 1);

    /**
     * 指令表達式解析,即 v-text='counter' counter的解析
     * 這裏暫時只考慮包含key的狀況
     */
    var key = attr.value;
    return directiveDef
        ? {
            attr: attr,
            key: key,
            dirname: directiveName,
            definition: directiveDef,
            argument: arg,
            /**
             * 指令自己是一個函數的狀況下,更新函數即它自己,不然調用它的update方法
             */
            update: typeof directiveDef === 'function'
                ? directiveDef
                : directiveDef.update
        }
        : null;
}

/**
 * 對象合併
 */
function extend (child, parent) {
    parent = parent || {};
    child = child || {};

    for(var key in parent) {
        if (parent.hasOwnProperty(key)) {
            child[key] = parent[key];
        }
    }

    return child;
}

參考來源:https://github.com/xiaofuzi/d...git

相關文章
相關標籤/搜索