the super tiny vue.js(200行原始碼)

yangxiaofu發表於2016-11-13

Online demo:http://yangxiaofu.com/deep-in-vue/src/the-super-tiny-vue.html github: https://github.com/xiaofuzi/deep-in-vue/blob/master/src/the-super-tiny-vue.js es6版本:https://github.com/xiaofuzi/re-vue

the super tiny vue.js. 程式碼總共200行左右(去掉註釋)

簡介:一個迷你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>

var vm = new Vue({
    id: 'counter',
    data: {
        counter: 1
    },
    methods: {
        add: function () {
            this.counter += 1;
        }
    }
})

如上為一段模板以及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屬性值。

    1. 響應式資料與節點的繫結(bindAccessors) 響應式資料這裡拆分為 data 和 methods 物件,分別用來儲存資料值和方法。

    var vm = new Vue({ id: 'counter', data: { counter: 1 }, methods: { add: function () { this.counter += 1; } } })

    我們上面解析得到 v-model 對於的指令值為 counter,所以這裡將data中的counter與當前節點繫結。

    通過2、3兩步實現了型別與 textDirective->el<-data.counter 的關聯,當data.counter發生set(具體檢視defineProperty set 用法)操作時, data.counter得知自己被改變了,所以通知el元素需要進行更新操作,el則使用與其關聯的指令(textDirective)對自身進行更新操作,從而實現了資料的 響應式。

    • textDirective
    • el
    • data.counter 這三個是繫結的主體,資料發生更改,通知節點需要更新,節點通過指令更新自己。
  • 4.其它相關操作

    var prefix = 'v';
    var Directives = {
    /**
     *對應於 v-text 指令
    */
    text: function (el, value) {
            el.textContent = value || '';
    },
    /**
     *對應於 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 = {}; /** * 指令處理 */ [].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]; } } /** * ready methods */ if (opts.ready && typeof opts.ready == 'function') { this.ready = opts.ready; this.ready(); } function processNode (el) { getAttributes(el.attributes).forEach(function (attr) { var directive = parseDirective(attr); if (directive) { bindDirective(self, el, bindings, directive); } }) } } /************************************************************** * @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.value); /** * 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; }

相關文章