前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >new一个Vue

new一个Vue

作者头像
ayqy贾杰
发布2019-06-12 12:21:46
4650
发布2019-06-12 12:21:46
举报
文章被收录于专栏:黯羽轻扬

感谢支持ayqy个人订阅号,每周义务推送1篇(only unique one)原创精品博文,话题包括但不限于前端、Node、Android、数学(WebGL)、语文(课外书读后感)、英语(文档翻译) 如果觉得弱水三千,一瓢太少,可以去 http://blog.ayqy.net 看个痛快

一.核心结构

Vue的数据绑定机制:

代码语言:javascript
复制
setter+脏检查+发布订阅管理

从0.x开始就是这样,dep.js、watcher.js、observer.js:

代码语言:javascript
复制
Subject: dep.js
Observer: watcher.js(内置脏检查)
Setter: observer.js(set时触发Subject.notify)

通过定义setter来监听数据变化,那么就有个很重要的问题:对于深层数据结构,也挨个定义setter吗?

确实是这样:

代码语言:javascript
复制
是数组的话,挨个observe定义setter,深度递归监听所有Object的key
被摸过的数据身上都有__ob__

感觉好像存在内存爆炸的问题,传入一个超大号数据对象的话,单是getter/setter就得占用不少空间,但实际场景很难遇到整页是一大坨数据的,对于重数据的场景,一般会抽离出数据层,由专门的状态管理机制来拆分数据,这样就很难发生内存爆炸了

最关键的部分就是这些,要能跑起来还需要Compiler & Directive编译转换源码,结构如下:

代码语言:javascript
复制
       解出关系
      创建View             监听变化
tpl —— Compiler —— Subject & Observer & Manager
          |
      Directive

输入tpl & data,输出view,并建立data-view的联系

二.框架

会说话的代码如下:

代码语言:javascript
复制
// 从模板解析出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
};

输入是这样子:

代码语言:javascript
复制
<!-- 模版 -->
<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

代码语言:javascript
复制
// 从模板解析出data-view的关系
var Compiler = function(tpl) {
   // 模板编译,转dom操作
};

编译器解析模版,应该输出结构信息,那么定义NodeMeta

代码语言:javascript
复制
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}
   // }
};

核心任务是解析模版:

代码语言:javascript
复制
Compiler.prototype.parse = function() {
   // 提取标签名,创建meta树
   this.nodeMeta = this.matchTag();
};
Compiler.prototype.matchTag = function() {
   var rootMeta;
   //...创建结构树
   return rootMeta;
};

得到结构meta树之后,该创建View了:

代码语言:javascript
复制
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

指令是辅助编译器的,负责处理一些复杂的东西:

代码语言:javascript
复制
var Directive = function(directive) {
   // 配合compiler,处理复杂一些的DOM操作(repeat, on, bind),建立data-view的关联
};

既然是辅助编译器,那也要负责创建View

代码语言:javascript
复制
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

代码语言:javascript
复制
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

发布订阅模式(观察者模式)中的主题(报纸):

代码语言:javascript
复制
// 监听数据变化,实现data-view的绑定关系
var Subject = function() {
   // 主题
   this.obs = [];
};

只需要实现几个基本接口:

代码语言:javascript
复制
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实例,定义如下:

代码语言:javascript
复制
var Observer = function(updateFn) {
   // 观察者
   if (typeof updateFn === 'function') {
       this.update = updateFn;
   }
};
Observer.prototype.update = function() {};

报纸发布信息时,通知所有订报的人。这里观察者模式用来维护data-view的一对多关系

Manager

Manager是把通用的观察者模式与实际场景连接起来的东西,这里主要负责定义setter

代码语言:javascript
复制
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部分:

代码语言:javascript
复制
var Manager = function(data) {
   this.data = data;
   this.dep = new Subject();
   // 定义setter,管理Subject和Observer
   this.observe(data);
};

持有一个Subject实例,后续给它添加Observer,没什么好说的。深度定义setter实现起来也比较容易:

代码语言:javascript
复制
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

代码语言:javascript
复制
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个正则提取出需要的各部分:

代码语言:javascript
复制
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年…哈哈)

这里只实现了简单的模版解析,偷懒不支持裸文本节点,源码比较长,这里只给出关键部分:

代码语言:javascript
复制
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并建立数据绑定

源码太长,简单过程如下:

代码语言:javascript
复制
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指令为例:

代码语言:javascript
复制
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取出来,性能爆炸,不过这一步可以在编译阶段做,没关系

入口

到这里差不多完成了,开放入口,把流程串起来:

代码语言:javascript
复制
// 入口
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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2017-02-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一.核心结构
  • 二.框架
    • Compiler
      • Directive
        • Subject & Observer
          • Manager
          • 三.具体实现
            • 监听数据变化
              • 解析模版
                • 创建View并建立数据绑定
                  • 入口
                  • 四.在线Demo
                    • 写在最后
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档