본문으로 건너뛰기

Vue new 하기

무료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의 관계를 구축해야 합니다. 이벤트는 특수한데, handlerdata를 변경할 수 있기 때문에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의 1 대 다 관계를 유지하는 데 사용됩니다

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-view, view-data관계 구축

  • 엔트리

데이터 변화 감시는 매우 쉽고, 금방 완료됩니다. 템플릿 해석은 중요하지만 결정적인 부분은 아니며, 복잡도는 보통입니다. View 를 생성하고 데이터 바인딩을 구축하는 것이 가장 결정적인 부분이며, 가장 복잡합니다. 물론, 마지막으로 엔트리 포인트를 열어야 합니다

데이터 변화 감시

Subject & Observer부분은 위에 있으며, 더 이상 움직일 필요가 없습니다. 일반적인 것은 쉽게 완료됩니다

그럼 주로Managersetter정의 부분입니다:

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);
};

첫 렌더링 시dataobserver를 추가하면 후속 데이터 변화를 얻을 수 있습니다. 이렇게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관계를 업데이트하기 때문입니다

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

댓글

아직 댓글이 없습니다

댓글 작성