new一个Vue

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

一.核心结构

Vue的数据绑定机制:

setter+脏检查+发布订阅管理

从0.x开始就是这样,dep.js、watcher.js、observer.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

本文分享自微信公众号 - 前端向后(backward-fe),作者:ayqy

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-02-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • React SSR 源码剖析

    上篇React SSR 之 API 篇细致介绍了 React SSR 相关 API 的作用,本篇将深入源码,围绕以下 3 个问题,弄清楚其实现原理:

    ayqy贾杰
  • this类型_TypeScript笔记11

    返回类型是this,表示所属类或接口的子类型(称之为有界多态性(F-bounded polymorphism)),例如:

    ayqy贾杰
  • delete的奇怪行为

    F的实例拥有一个value属性,但不希望在new的时候就初始化属性值(因为这个值不一定用得到,而且计算成本比较高,或者new的时候还不一定能算出来),那么自然想...

    ayqy贾杰
  • 什么是BOM

    BOM(Browser Object Model)即浏览器对象模型,它提供了独立于内容而与浏览器窗口进行交互的对象,其核心对象是 window。

    清出于兰
  • JavaScript 箭头函数不完全指北

    我们可以使用 babel 转译器将 ES6 代码转译为 ES5代码, ES5代码如下

    撸码那些事
  • JS常用代码块

    版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。

    奋飛
  • ol4通过ImageCanvas实现大量点的展示以及交互的实现

    在ol4里面可以通过Vector Layer的方式进行点的渲染,但是当点的个数比较多的时候,会存在明显的操作不流畅。本文讲述如何利用ImageCanvas接口,...

    lzugis
  • JavaScript最全编码规范(精)

    更多信息指引:JavaScript Scoping & Hoisting by Ben Cherry.  比较运算符&相等

    山河木马
  • JavaScript命令模式

     步骤二,为对象(执行者)建立命令访问库 ---意思是可以通过extcute方法访问到addFlow方法

    wfaceboss
  • ES6 学习笔记之函数的拓展

    本文记录了一些 ES6 函数相关的改动,比较重要的就是箭头函数及箭头函数内部 this 的变化,其他一些不常见的概念我也仅仅是看了看,并没有实际操作测试效果。待...

    我与梦想有个约会

扫码关注云+社区

领取腾讯云代金券