Skip to content

YuYang019/simple-vue

Repository files navigation

极简版vue

2017/5/28

之前做到了这个题目,但是感觉实现的很烂,所以重新做了一遍,参(chao)考(xi)了vue1.x的源码,重点关注的是双向数据绑定的实现,说说思路吧

重点是Observer, Dep, Watcher, Compile,Batcher这几个构造函数

Observer: 改造数据对象,使set和get能够被监听,然后我们就能做一些事情了

Dep: 依赖容器,每个数据对应一个Dep容器,它存在于闭包之中,在改造数据的时候被创建,容器里装的是watcher,watcher在数据get的时候被隐式添加进dep。

watcher: 观察者,绑定节点与数据,当数据的set被触发的时候,依次调用该数据dep容器里的watcher的更新函数,这样就能实现视图和数据的更新。

compile: 编译函数,在初始化的时候,遍历dom各节点,对每个有模板或者命令的节点都创建一个watcher,并绑定该节点与watcher的关系,这样更新的时候就能精确的更新某个节点了,但是这个watcher如何添加进Dep依赖容器里呢,答案就是创建watcher的时候,watcher里会获取一次数据的值,触发get,watcher就被隐式添加到Dep里了。

Batcher: 批处理构造函数,保证如果同一数据同步的多次更新,DOM修改只改变一次

vue源码有很多精妙之处,让人大开眼界,比如:

  • 优雅的添加依赖,通过一个全局变量Dep.target来添加。以及隐式添加依赖
  • 如何通过路径,如 user.name 获取值,我原来是把这个拆开成user,name,然后通过递归获取,但源码直接用一个匿名函数return scope.user.name 把vm传进去,直接获取vm.user.name。多简单
  • compile函数的写法,转换成文档碎片。
  • Dep依赖容器存放于闭包之中,之前一直不知道如果要每个数据对应一个依赖容器的话,这个容器应该放在哪。

待改进:

  • 模版的解析不支持 {{user.name}}11 ,这样解析完后11会消失。

    思路:新建一个文档碎片,替换原文本。比如{{user.name}}111{{user.age}},用正则匹配后,创建三个新节点加入文档碎片,再用文档碎片替换原textnode,是模板的就会创建watcher,将该节点与数据绑定起来。这样原来只有一个文本节点,现在就有三个,并且各不干扰

  • 批处理dom

    目的:新建一个批处理类,它的作用是保证对于一个数据,不管它变化了多少次,对DOM的更新只进行一次。

     //比如这三条同步语句,我们需要做到只更新一次DOM,令user.name = 'c'
     user.name = 'a'; 
     user.name = 'b';
     user.name = 'c'

    主要思路:拿上面的代码举例,每次更改数据的时候,都会执行watcher的update函数,现在我们不直接在update里更新DOM,而是把watcher交给批处理类,让它决定怎么更新。既然要只更新一次,那么批处理的队列里只能有一个更新函数。而我们修改了三次数据,就交给了批处理类三次watcher。怎么保证只有一个更新函数呢?虽然修改了三次数据,但是这三次的数据的watcher是同一个,所以我们用它的id来保证更新函数的唯一性。当push的时候,检查是否存在相同id的watcher,不存在就添加进队列,存在就不处理。所以这个watcher的更新函数只会有一个。注意我们使用了setTimeout(function(){},0),这个的意思是等待主线程执行完毕之后才执行更新,所以更新函数的执行总是在修改数据之后,这就保证了更新的是最新的数据。

  • 数组处理

    目的:能够监听数组的改变,并显示在视图

    思路:由于数组的性质比较特殊,用户对数组的增删是高频的,如果对数组使用defineProperty,会带来性能问题和极大增加代码复杂度,所以需要对数组操作进行代理,改造它的push,pop等方法,如果对数组进行这些操作,就通知watcher更新视图

至于vue2.x的virtual dom,还得研究研究...

-----------------分割线-------------------------------------------------------

2017/7/2

使用了ES6 module重构了一下,尝试模块化开发

发现现在知道这么写但是不知道为什么,所以我尝试从最朴素的需求推导出这些架构

那么就从最基本的想起,逐步解决问题,实现最简单的功能

  • 现在实现第一个功能:一个对象,我能够监听它的变化。

这个无需多言,使用 Object.defineProperty 便能监听对象的set和get,所以我需要一个类来遍历改造这个对象,让它的所有值都能被监听。那么这个类便是Observer了,遍历的思想是利用了递归

  • 第二个功能:这个数据对象能和视图联系起来,如果数据改变视图也会改变

那么我们需要一个东西,它能够将数据和视图联系起来。所谓的视图是什么呢,在这里就是html里的DOM节点,在DOM里我们用{{}}括起来的表示要显示的数据,如果数据改变了,这个DOM里的文本就要跟着改变,所以,将视图和数据联系起来,就是将一个数据和一个DOM节点联系起来。所以我们需要一个类来联系它们,这就是watcher,当数据更新时,watcher更新这个DOM节点的值

但是我怎么知道这个节点在哪?这就需要解析整个html文本,当解析到{{}}的时候,就知道这是一个模板了,然后创建watcher将它们联系起来,而这个解析的过程就是Compile类负责

  • 第三个问题:watcher有了,但是存在哪里?

一个数据可以对应多个节点,也就是对应多个watcher,我们在compile的时候创建了watcher,如果不给这把这些watcher存起来,那就没办法使用它们,也就不能实现更新。所以我们需要给整个数据对象的每一个数据都建立一个对应的储存容器,这样当某一个数据改变的时候,就依次调用对应容器里的watcher的更新函数就行了。这个容器就是Dep

但是这个Dep容器又要放到哪呢?我们期望当一个数据触发set的时候,能顺着作用域链直接找到这个Dep容器,不需要单独拿出来放到一个命名空间里,不然太过于繁琐而且不好控制。那么答案显而易见了,由于我们是递归遍历这个数据对象,所以遍历每一层的时候,可以创建一个Dep,存于闭包之中,这样set的时候可以通过作用域链找到这个容器,而且不需要做多余的处理,十分简洁

  • 第四个问题: 容器有了,watcher有了,但是怎么往对应容器里添对应watcher?

首先我们得知道这是哪一个数据的容器,这是编译的时候,通过{{}}模板里的表达式获得的。知道了是哪一个数据,那么要操作这个容器,需要触发这个数据的set或者get,然后通过set或者get的作用域链找到这个容器,然后添加,这里无疑选择触发get。为了保证是一个个的添加,所以把这个watcher赋给一个全局变量Dep.target,等待添加完了,置空,下一次添加时再将下一个watcher再赋给Dep.target。每次只添一个

所以单个watcher添加流程大致如下,创建watcher --> watcher内部通过传入的表达式触发对应数据的get --> 全局变量Dep.target被赋值,此时为该watcher --> 由get找到依赖容器,将Dep.target添加进去 --> 添加完成,Dep.target置空 --> 下一个watcher重复以上步骤

这里依赖的添加都是隐式的,不需要额外的操作,创建watcher的时候,就会自动添加依赖,不得不感叹原作者的巧妙构思

  • 第五个问题:批处理?

就是同一个数据多次更新DOM只更新最新的一次,这个上面说过了,就不赘述了,这个就是Batcher类

  • 第六个问题:数组如何监听?

前面用defineProperty实现了对对象的监听,那么数组要如何处理?我们可以对数组操作进行代理,如果有相关操作,例如push,就调用我们自定义的方法而不是用原生方法,在自定义方法里通知watcher进行更新,代理有两种方式,一是直接覆盖数组的__proto__,二是循环把每个方法写进数组对象

对象利用defineProperty添加watcher依赖和监听变化,用一个api实现2个功能

而数组是利用defineProperty添加watcher,再利用代理监听变化。

  • 第七个问题:数组的dep应该存在哪?

在对象中,我们把容器存于闭包之中,通过defineProperty监听,一旦get或者set就能够获取dep容器。可是对于数组来说,由于不使用defineProperty监听其变化,我们在每一数据对象新增加一个__ob__属性,这是一个Observer类,dep存于这个__ob__里。如果数组改变,找到该数组的__ob__属性的dep,依次调用里面的watcher更新视图

数组的watcher的怎么加进容器?

数组watcher添加流程和第四个问题里的流程差不多,都是利用defineProperty,也是触发get隐式添加依赖,值得一提的是数组的容器dep如何创建。不是简单地new Dep(),这里比较绕,下面列出关键部分

	// Observer类里的defineReactive方法
	var childObj = observe(val) // 递归遍历数据对象,并返回一个Observer

	// observe方法
	ob = new Observer(val)
	return ob

	// Observer构造函数
	def(val, '__ob__', this) // 在数组或对象里定义__ob__属性,并把自己添加进去

	// 依赖添加
	if (childObj) {
		childObj.dep.depend() // childObj是一个Observer,也是该数组的__ob__,往这里面添加依赖
	}

数组的依赖容器就是这个childObj.dep

主要思路是:递归遍历数据对象,把每一层new的Observer返回,赋值给childObj,这个childObj也是存于闭包之中,由于创建Observer的时候,它把自己添加进了数组里命名为__ob__,这样childObj如果改变,数组里的__ob__也会改变,它们其实指向同一个地址。

注意必须要明白一点:对数组来说,defineProperty只用于添加依赖,与更新无关,更新是从__ob__中拿到容器进行通知,而添加依赖是往存于闭包中的childObj加,childObj与__ob__指向同一个地址

虽然看得懂,但是感觉要我自己写的话完全想不到啊。。。

以上简单分析了一下每个类的由来,部分细节处理并没有提及,但是理解起来清晰了很多

-----------------分割线-------------------------------------------------------

2018/6 我又来了,过了这么久,看以前写的东西有些感叹。。不知不觉过了这么久。。随便写写vue2引入vdom的好处

  1. 紧跟潮流,毕竟react提出vdom这个概念的确很有噱头
  2. 有利于跨平台,vdom并不必须依赖于浏览器,它可以理解为页面结构的映射,那么这个映射完全可以转换成适应别的平台的结构
  3. 从vue自身角度来看,vdom有优势。从vue1.x可以看出,它是非常细粒度的更新,因为它是遍历整个dom将数据和node节点通过watcher连接,当数据变更时,直接更新节点。这样的问题在于watcher可能会非常多。当页面非常复杂的时候,性能会很差。Vue2引入了虚拟dom,由细粒度更新变为中粒度的更新,不再通过watcher直接更改节点,而是通过watcher触发diff过程来更新。可以说大大提升了性能

About

learn the principle of vue1.x

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published