跳到主要內容
黯羽輕揚每天積累一點點

new 一個 Vue

免費2017-02-26#JS#vue源码分析#vue diff#vue数据绑定原理#vue数据绑定性能

搓一個 Vue... 的 core,從 Compiler & Directive 到 Watcher,實現雙向資料綁定

一。核心結構

Vue 的資料綁定機制:

setter+ 髒檢查 + 發布訂閱管理

從 0.x 開始就是這樣,dep.jswatcher.jsobserver.js

Subject: dep.js
Observer: watcher.js(內建髒檢查)
Setter: observer.js(set 時觸發 Subject.notify)

透過定義 setter 來監聽資料變化,那麼就��個很重要的問題:對於深層資料結構,也挨個定義 setter 嗎?

確實是這樣:

是陣列的話,挨個 observe 定義 setter,深度遞迴監聽所有 Object 的 key
被摸過的資料身上都有__ob__

感覺好像存在記憶體爆炸的問題,傳入一個超大號資料物件的話,單是 getter/setter 就得佔用不少空間,但實際場景很難遇到整頁是一大坨資料的,對於重資料的場景,一般會抽離出資料層,由專門的狀態管理機制來拆分資料,這樣就很難發生記憶體爆炸了

最關鍵的部分就是這些,要能跑起來還需要 Compiler & Directive 編譯轉換原始碼,結構如下:

       解出關係
       建立 View             監聽變化
tpl —— Compiler —— Subject & Observer & Manager
           |
       Directive

輸入 tpl & data,輸出 view,並建立 data-view 的聯絡

二。框架

會說話的程式碼如下:

// 從模板解析出 data-view 的關係
var Compiler = function(tpl) {
    // 模板編譯,轉 dom 操作
};
var Directive = function(directive) {
    // 配合 compiler,處理複雜一些的 DOM 操作(repeat, on, bind),建立 data-view 的關聯
};

// 監聽資料變化,實現 data-view 的綁定關係
var Subject = function() {
    // 主題
    this.obs = [];
};
var Observer = function(updateFn) {
    // 觀察者
};
var Manager = function(data) {
    // 定義 setter,管理 Subject 和 Observer
};

輸入是這樣子:

<!-- 模版 -->
<div id="demo" v-cloak>
    <h1 v-bind:style="{ items.length ? 'border-bottom: 1px solid #ddd' : 'border: none' }">
    {{title}}
    </h1>
    <p v-if="!items.length">empty</p>
    <ul v-for="item in items">
        <li v-on:click="item.a[1].a[1].a.a++" style="background-color: #2b80b6">
            {{item.a[1].a[1].a.a}}
        </li>
    </ul>
</div>

// 資料
var data = {
    title: 'list',
    items: [{a: [0, {a: [1, {a: {a: 1}}]}]}]
};
var v = new V({
    el: '#demo',
    data: data,
    created: function() {
      console.log(data);
    }
});

看起來還有很遠的路要走,一眼望不到邊,稍微細化一下

Compiler

// 從模板解析出 data-view 的關係
var Compiler = function(tpl) {
    // 模板編譯,轉 dom 操作
};

編譯器解析模版,應該輸出結構資訊,那麼定義 NodeMeta

Compiler.NodeMeta = function(tag) {
    // Compiler 要輸出的 DOM 結構 meta 格式
    // {
    //   tag: 'ol',
    //   children: [NodeMeta, NodeMeta...],
    //   props: [{key: 'id', value: 'ol'}...],
    //   directives: [{name: 'v-if', value: '!items.length'}],
    //   // 文字節點情況比較複雜,這裡不考慮非資料綁定形式的文字和沒有被單獨包起來的文字
    //   textContent: 'item.text',
    //   // for 指令會擴充套件子級作用域
    //   extraScope: {'item': Object}
    // }
};

核心任務是解析模版:

Compiler.prototype.parse = function() {
    // 提取標籤名,建立 meta 樹
    this.nodeMeta = this.matchTag();
};
Compiler.prototype.matchTag = function() {
    var rootMeta;
    //... 建立結構樹
    return rootMeta;
};

得到結構 meta 樹之後,該建立 View 了:

Compiler.prototype.render = function(vm) {
    this.vm = vm;
    var render = function(nodeMeta) {
        //... 解析指令
        //... 建立節點
        //... 設定 attr,綁定事件 handler,實現 view-data 的響應
    };
    var node = render(this.nodeMeta);
    // 用建立好的節點替掉模版元素
    vm.el.parentNode.replaceChild(node, vm.el);
};

Directive

指令是輔助編譯器的,負責處理一些複雜的東西:

var Directive = function(directive) {
    // 配合 compiler,處理複雜一些的 DOM 操作(repeat, on, bind),建立 data-view 的關聯
};

既然是輔助編譯器,那也要負責建立 View

Directive.prototype.render = function(vm, node, nodeMeta, compilerRender) {
    switch (dir) {
        case 'for':
            //... 建立多組節點
            break;
        case 'if':
            //... 條件建立
            break;
        case 'on':
            //... 綁定事件 handler,建立 view-data 的關聯
            break;
        case 'bind':
            //... 簡單的表示式求值
            break;
        case 'cloak':
            //... 不用管
            break;
        default:
            console.error('unknown directive: ' + key);
    }
};

各種指令預設都要建立 data-view 的關聯,事件比較特殊,因為 handler 可能會改變 data,也就相當於 view 改變,data 要跟著變,所以事件指令還要負責完成 view-data 的關聯

比起編譯器,指令複雜的地方在於需要做表示式求值,以及建立 handler

Directive.getParams = function(vm, extraScope) {
};
Directive.createFn = function(vm, fnStr, extraScope) {
    var handler = function() {
        var args = [].slice.call(arguments);
        var param = Directive.getParams(vm, extraScope);
        var fnBody;
        //... 填充函式定義的各部分
        var fn = eval(fnBody);
        return fn.apply(vm, args.concat(param[1]));
    };
    return handler;
};

Subject & Observer

發布訂閱模式(觀察者模式)中的主題(報紙):

// 監聽資料變化,實現 data-view 的綁定關係
var Subject = function() {
    // 主題
    this.obs = [];
};

只需要實現幾個基本介面:

Subject.prototype.add = function(ob) {
    this.obs.push(ob);
};
Subject.prototype.remove = function(ob) {
    var index = this.obs.indexOf(ob);
    if (index !== -1) this.obs.splice(index, 1);
};
Subject.prototype.notify = function(lastValue, newValue) {
    this.obs.length && this.obs.forEach(function(ob) {
        ob.update();
    });
};

參數 ob 就是觀察者(訂報的人)Observer 實體,定義如下:

var Observer = function(updateFn) {
    // 觀察者
    if (typeof updateFn === 'function') {
        this.update = updateFn;
    }
};
Observer.prototype.update = function() {};

報紙發布資訊時,通知所有訂報的人。這裡觀察者模式用來維護 data-view 的一對多關係

Manager

Manager 是把通用的觀察者模式與實際場景連線起來的東西,這裡主要負責定義 setter

var Manager = function(data) {
    this.data = data;
    this.dep = new Subject();
    // 定義 setter,管理 Subject 和 Observer
    this.observe(data);
};
Manager.prototype.observe = function(obj) {
};
Manager.prototype.observeArray = function(arr) {
};

遞迴定義 setter,嵌入資料變化 hook

三。具體實現

大致分為 3 部分:

  • 監聽資料變化

  • 解析模版,找出 viewdata 的聯絡

  • 建立 View 並建立 data-viewview-data 的關係

  • 入口

監聽資料變化非常容易,分分鐘搞定;解析模版是重要但不關鍵的部分,複雜度一般;建立 View 並建立資料綁定是最關鍵的部分,也最複雜;當然,最後還需要開一個入口

監聽資料變化

Subject & Observer 的部分就在上面,已經不需要動了,通用的很容易搞定

那麼主要是 Manager 定義 setter 部分:

var Manager = function(data) {
    this.data = data;
    this.dep = new Subject();
    // 定義 setter,管理 Subject 和 Observer
    this.observe(data);
};

持有一個 Subject 實體,後續給它新增 Observer,沒什麼好說的。深度定義 setter 實現起來也比較容易:

Manager.prototype.observe = function(obj) {
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            // 先監聽孩子的
            if (typeof obj[key] === 'object') {
                if (Array.isArray(obj[key])) {
                    self.observeArray(obj[key]);
                }
                else {
                    self.observe(obj[key]);
                }
            }
        }
        // 定義 setter
        void function() {
            // 隔離一個 value
            var value = obj[key];
            Object.defineProperty(obj, key, {
                set: function(newValue) {
                    if (typeof newValue === 'object' || newValue !== value) {
                        console.log('data 變了', value, newValue);
                        value = newValue;
                        // setter 通知變化
                        obj.__ob__.dep.notify(value, newValue);
                    }
                    return newValue;
                }
            });
        }();
    }
};

陣列的話,遍歷並 observe

Manager.prototype.observeArray = function(arr) {
    arr.forEach(function(data) {
        if (typeof data === 'object') {
            if (Array.isArray(data)) {
                self.observeArray(data);
            }
            else {
                self.observe(data);
            }
        }
    });
};

P.S. 這裡的實現與 Vue不太一樣,簡單起見

解析模版

讀模版,找出 dataview 的關係

n 個正則提取出需要的各部分:

var Compiler = function(tpl) {
    // 模板編譯,轉 dom 操作
    this.tpl = tpl.trim();

    this.REGEX = {
        tag: /<([^>/\s]+)[^<>]*>/gm,
        attrTag: /<([^>/\s]+)\s+([^>]+)>/gm,
        text: /<([^>\s]+).*>\s*{{([^}]*)}}\s*<\/\1>/gm,
        attr: /(?:([^="\s]+)(?:="([^"]+)")?)/gm
    };
};

P.S. 說實話,這裡還是比較費勁的(師父說需要 1 年... 哈哈)

這裡只實現了簡單的模版解析,偷懶不支援裸文字節點,原始碼比較長,這裡只給出關鍵部分:

Compiler.prototype.matchTag = function() {
    var openTagStack = [], peak;
    while ((tmp = this.REGEX.tag.exec(str)) !== null) {
        newMeta = new NodeMeta(tag);
        if (lastEndIndex > 0) {
            // 構造 nodeMeta 樹
            peak = openTagStack[openTagStack.length - 1];
            closeTagRegex = new RegExp('</' + peak + '>', 'm');
            skipMatch = str.substring(lastEndIndex, tmp.index);
            newMeta.parent = meta;
            // 預設進入下一層
            if (meta.children.length > 0){
                meta = meta.children[meta.children.length - 1];
            }
            // 匹配一個就出一層
            if (closeTagRegex.test(skipMatch)) {
                openTagStack.pop();
                meta = meta.parent;
            }
            meta.children.push(newMeta);
        }
        openTagStack.push(tag);

        // 填充 props & directives
        attrs = this.matchAttr(thisMatch);

        // 填充 textContent
    }

    return rootMeta;
};

到這裡就得到結構 meta 樹了,下面要拿著這份配置資料去建立 View:

建立 View 並建立資料綁定

原始碼太長,簡單過程如下:

Compiler.prototype.render = function(vm) {
    var render = function(nodeMeta) {
        // tag
        var node = document.createElement(nodeMeta.tag);
        // props
        node.setAttribute(prop.key, prop.value);
        // textContent
        var fn = function() {
            var exp = Directive.createFn(vm, nodeMeta.textContent, nodeMeta.extraScope);
            var text = exp();
            node.innerText = text || "";
        }
        //!!! 實現 data-view 的綁定
        vm.data.__ob__.dep.add(new Observer(fn));
        // directives
        var directive = nodeMeta.directives[i];
        var d = new Directive(directive);
        // 指令 render 返回 false 表示不需要渲染 node 及 children
        // 返回 true 表示已經把 children 渲染好了
        var renderOrNot = d.render(vm, node, nodeMeta, render);
        // children
        var childNode = render(meta);
        childNode && node.appendChild(childNode);

        return node;
    };
    // 建立 View,替掉模版元素
    var node = render(this.nodeMeta);
    vm.el.parentNode.replaceChild(node, vm.el);
};

首次渲染時給 data 新增 observer,後續資料變化就能拿到了,這樣就實現了 data-view 的綁定

起重要輔助作用的 Directive 如下,太長,這裡以 on 指令為例:

Directive.prototype.render = function(vm, node, nodeMeta, compilerRender) {
    var dir = this.REGEX.directive.exec(key)[1];
    var event, handler, exp, prop, propValue;
    switch (dir) {
        case 'on':
            event = this.REGEX.key.exec(key);
            if (event) {
                event = event[1];
                handler = Directive.createFn(vm, value, nodeMeta.extraScope);
                node.addEventListener(event, function() {
                    handler();
                });
            }
            break;
    }
};

建立 handler,並 addEventListener,在透過定義 setter 實現資料變化監聽的情況下,view-data 的綁定是天然的,不需要額外處理,因為只要 handler 執行時改變了 data,就會觸發 setter,進而 notify 建立 View 時建立的 data-view 的關係,更新 View

P.S. 建立 handler 的部分比較挫,拼一個 new Function() 定義,再 eval 取出來,性能爆炸,不過這一步可以在編譯階段做,沒關係

入口

到這裡差不多完成了,開放入口,把流程串起來:

// 入口
var V = function(config) {
    // 基本配置資料(view, data)
    this.el = el;
    this.data = config.data;
    this.methods = config.methods;
    // 生命週期 hook
    var LIFE_CYCLES = ['created'];

    this._init();
};
V.prototype._init = function() {
    // 監聽資料變化
    this._observe();
    console.log(this.data);
    // 解析關係,轉 DOM 操作
    this.compiler = this._render();
};
V.prototype._render = function() {
    var c = new Compiler(this.el.outerHTML);
    c.parse();
    c.render(this);
    return c;
};
V.prototype._observe = function() {
    this.__ob__ = new Manager(this.data);
};

四。在線 Demo

Demo 地址:http://ayqy.net/temp/data-binding/vue/index.html

P.S. 原始碼都在原始碼裡,註釋非常清楚

寫在最後

生活總有灰色的部分,看不見光,���找不到方向。但好在路在腳下,沒有初心沒有將來都沒有關係,在路上就好,keep up

評論

暫無評論,快來發表你的看法吧

提交評論