new一个Vue

一.核心结构

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code