Vue.js 代码实现

检验学习效果的最好方法就是自己造轮子。最近在学习Vue源码,写了一个迷你版vue,实现数据响应式。从step1到step3.2,是开发步骤和实现思路,每一步都可以独立运行。

代码地址:https://github.com/dora-zc/miniature-vue

目录结构

├── README.md

├── step0

│ └── defineProperty_test.html

├── step1

│ ├── XVue.js

│ └── index.html

├── step2

│ ├── XVue.js

│ └── index.html

├── step3.1

│ ├── XVue.js

│ ├── compile.js

│ └── index.html

└── step3.2

​ ├── XVue.js

​ ├── compile.js

​ └── index.html

以上每个step文件夹对应下面的每一步骤,代表了代码实现的顺序,每个文件夹下的代码都可以独立运行。

1. 步骤一

创建XVue.js。

创建Vue类,通过Observer劫持监听所有属性。

observe函数的作用:递归遍历data选项,它当中的defineReactive函数为data中每一个key定义getter和setter,达到数据劫持的目的。

步骤一对应代码目录:step1

2. 步骤二

处理页面上的<div>{{msg}}</div>,也就是收集依赖,当msg的值发生变化时,视图需要做出相应的变化。因此需要创建依赖管理器,把所有依赖保存起来,当数据发生变化的时候再去更新对应的依赖。

2.1 创建Dep类

Dep负责将视图中的所有依赖收集管理,包括依赖添加和派发通知

1- 在Dep类中创建数组deps=[],用来存放Watcher的实例

2-创建addDep方法,添加Watcher

3-创建notify方法,通知所有的Wather执行更新。遍历deps数组,调用每个Wather的更新方法

2.2 创建监听器Watcher类

Watcher是具体的更新执行者。

1-将当前Watcher实例添加到Dep.target上。

Dep.target = this

之后在get时,就能通过Dep.target拿到当前Watcher的实例。

2-创建update方法

3-set方法中,调用dep.notify,让依赖管理器通知更新,则所有的Watcher会执行update方法

那么问题来了:Watcher在什么时候收集最合适?

在defineReactive函数的get方法中,get方法触发时,把Watcher放进Dep.target中。

那么问题又来了:为什么是在get方法中呢?

因为在扫描视图中的依赖时,如果扫描到<div>{{msg}}</div>,此时一定会去访问msg的值,就会触发get。一旦get被触发,就能将Watcher放进dep中,实现依赖收集的目的。所以get是一个合适的时间点。

代码测试:在get中输出dep.deps,如果Watcher已经放进去了,并且控制台打印出Watcher中的update方法中的log,说明这一步操作成功了。

至此,已经完成的工作如下:

步骤二对应代码目录:step2

现在,Watcher发生变化时,视图还没有更新,下面我们将要完成视图更新的操作。

首先,需要Compile对界面模板解析指令,进行编译,编译的阶段实际是创建Watcher的阶段。Watcher是由编译器创建的。编译器在做依赖收集的时候,顺便把Watcher创建了。Watcher在创建的时候,立刻就能知道它将来要更新的是谁,它应该被谁管理,它发生变化以后值应该是什么。于是Watcher就知道调谁(Updater去做更新了)。

3.步骤三

创建compile.js,用于扫描模板中所有依赖(指令、插值、绑定、事件…),创建更新函数和Watcher

3.1 扫描模板

1-创建编译器Compile类,接收两个参数,el(宿主元素或选择器)和vm(当前vue实例)

2-创建node2Fragment函数,将dom节点( $el )截成代码块( 转换为Fragment )来处理,而不是直接做dom操作,提高执行效率

3-创建compile函数,执行编译( 将模板中的动态值替换为真实的值 ),传入代码块

4-将生成的结果追加至宿主元素

3.1.1 node2Fragment函数

创建一个新的fragment,将原生节点移动至fragment

返回fragment,传给编译函数进行编译

3.1.2 compile函数

获取所有的孩子节点,进行遍历,判断节点类型,并作出相应的判断

处理元素节点

处理文本节点( 只处理{{msg}} 这种情况,其他的全部不处理)

...其他的节点类型暂时不判断了

遍历可能存在的子节点,往下递归

下面是compile函数中的两个核心方法

1-compileElement方法:编译元素节点

<div v-text="test" @click="onClick">{{msg}}</div>

拿到所有属性名称,进行遍历

2-compileText方法:编译文本节点

代码测试:

在XVue constructor中,创建编译器实例,将宿主元素el和当前vue实例作为参数传入。

如果compileElement和compileText两个函数能触发,控制台打印出"开始编译元素节点"和"开始编译文本节点",则说明功能正常,可以继续让下走了。

对应代码:step3.1

3.2 编译元素节点和文本节点,并创建更新函数

3.2.1 编译元素节点compileElement方法实现

获取节点所有属性,进行遍历。判断指令和事件,已经相应的处理方法。

指令只试着处理v-text,v-html,v-model三个,其他的暂不处理

v-model:双向绑定还需要处理视图对模型的更新

3.2.2 创建更新器函数

更新器函数:接收四个参数,node,vm,exp,dir(指令)

针对指令的更新器主要是在做dom操作

在更新器函数中创建Watcher实例,当Watcher监听到变化的时候,就能触发视图的更新。

至此,全部代码已经完成,双向数据绑定顺利实现!

对应代码:step3.2

Vue.js 工作机制

初始化

在new Vue()之后,Vue会调用初始化函数,会初始化声明周期、事件、props、methods、data、computed和watcher等。其中最重要的是通过Object.defineProperty设置setter和getter,用来实现响应式和依赖收集。

初始化之后会调用$.mount挂载组件。

编译

编译模块分为三个阶段:

1-parse

使用正则解析模板中的vue的指令、变量等等,形成抽象语法树AST

2-optimize

标记一些静态节点,用作后面的性能优化,在diff的时候直接略过

3-generate

把第一步生成的AST转化为渲染函数 render function

响应式

这一块是vue最核心的内容。初始化的时候通过defineProperty进行绑定,设置通知的机制,当编译生成的渲染函数被实际渲染的时候,会触发getter进行依赖收集,在数据变化的时候,触发setter进行更新。

虚拟dom

虚拟dom是由react首创,Vue2开始支持,就是用JavaScript对象来描述dom结构,数据修改的时候,我们先修改虚拟dom中的数据,然后数组做diff算法,最后再汇总所有的diff,力求做最少的dom操作,毕竟js里对比很快,而真实的dom操作太慢了。

<div name="小菠萝" style="color:red" @click="xx">
<a>click me</a>
</div>
// vdom
{
tag:'div',
props:{
name:'小菠萝',
style: {color:red},
onClick:xx
},
children:[
{
tag:'a',
text:'click me'
}
]
}

更新视图

数据修改触发setter,然后监听器会通知进行修改,通过对比两个dom树,得到改变的地方,就是patch,然后只需要把这些差异修改即可。

编译

compile的核心逻辑是获取dom,遍历dom,获取{{}}格式的变量,以及每个dom的属性,截取v-和@开头的部分来设置响应式。

Vue.js 源码实现的更多相关文章

  1. vue.js源码精析

    MVVM大比拼之vue.js源码精析 VUE 源码分析 简介 Vue 是 MVVM 框架中的新贵,如果我没记错的话作者应该毕业不久,现在在google.vue 如作者自己所说,在api设计上受到了很多 ...

  2. 从template到DOM(Vue.js源码角度看内部运行机制)

    写在前面 这篇文章算是对最近写的一系列Vue.js源码的文章(https://github.com/answershuto/learnVue)的总结吧,在阅读源码的过程中也确实受益匪浅,希望自己的这些 ...

  3. Vue.js源码——事件机制

    写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出.文章的原地址:https://github.com/an ...

  4. 从Vue.js源码角度再看数据绑定

    写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出.文章的原地址:https://github.com/an ...

  5. vue源码分析—Vue.js 源码构建

    Vue.js 源码是基于 Rollup 构建的,它的构建相关配置都在 scripts 目录下.(Rollup 中文网和英文网) 构建脚本 通常一个基于 NPM 托管的项目都会有一个 package.j ...

  6. 2018-11-23 手工翻译Vue.js源码:尝试重命名标识符与文本

    续前文: 手工翻译Vue.js源码第一步:14个文件重命名 对core/instance/索引中的变量, 方法进行重命名如下(题图): import { 混入初始化 } from './初始化' im ...

  7. 【转】从Vue.js源码看异步更新DOM策略及nextTick

    在使用vue.js的时候,有时候因为一些特定的业务场景,不得不去操作DOM,比如这样: <template> <div> <div ref="test" ...

  8. vue.js源码学习分享(一)

    今天看了vue.js源码  发现非常不错,想一边看一遍写博客和大家分享 /** * Convert a value to a string that is actually rendered. *转换 ...

  9. Vue.js 源码分析(一) 代码结构

    关于Vue vue是一个兴起的前端js库,是一个精简的MVVM.MVVM模式是由经典的软件架构MVC衍生来的,当View(视图层)变化时,会自动更新到ViewModel(视图模型),反之亦然,View ...

  10. 嗨,让我带你逐行剖析Vue.js源码

    本项目受到了阮一峰老师的肯定,已刊登在阮一峰老师微信公众号的科技爱好者周刊第87期,同时也被多个微博大V转发,短短一个月时间内在github上star数量就已经突破2k! Hello,大家好,我最近在 ...

随机推荐

  1. IE6、IE7、IE8及其他浏览器多个元素并排显示

    IE6.IE7.IE8及其他浏览器多个元素并排显示 HTML代码 <div class="line"> <h1>全部input框</h1> &l ...

  2. Saiku_00_资源帖

    一.精选 1.李秋 随笔分类 - pentaho 二.概述 1.Saiku + Kylin 多维分析平台探索 三.Saiku+Kylin 1.使用Saiku+Kylin构建多维分析OLAP平台 2.使 ...

  3. 原生JS实现淡入淡出效果(fadeIn/fadeOut/fadeTo)

    淡入淡出效果,在日常项目中经常用到,可惜原生JS没有类似的方法,而有时小的页面并不值得引入一个jQuery库,所以就自己写了一个,已封装, 有用得着的朋友, 可以直接使用. 代码中另附有一个设置元素透 ...

  4. 【转】C++11 标准新特性:Defaulted 和 Deleted 函数

    原文链接http://www.ibm.com/developerworks/cn/aix/library/1212_lufang_c11new/ 本文将介绍 C++11 标准的两个新特性:defaul ...

  5. stl_iterator.h

    stl_iterator.h // Filename: stl_iterator.h // Comment By: 凝霜 // E-mail: mdl2009@vip.qq.com // Blog: ...

  6. OpenCV——黑白调整

    参考算法: 闲人阿发伯的博客 // define head function #ifndef PS_ALGORITHM_H_INCLUDED #define PS_ALGORITHM_H_INCLUD ...

  7. BLOB (binary large object)

    BLOB (binary large object),二进制大对象,是一个可以存储二进制文件的容器. 在计算机中,BLOB常常是数据库中用来存储二进制文件的字段类型. BLOB是一个大文件,典型的BL ...

  8. 【Google】循环字符串里面的独立子串

    转载自九章算法(地址) 题目: 假设s是一个无限循环的字符串”abcdefghijklmnopqrstuvwxyz”,s就是一个”...zabcdefghijklmnopqrstuvwxyza...” ...

  9. [转]nodejs中的process模块--child_process.exec

    1.process是一个全局进程,你可以直接通过process变量直接访问它. process实现了EventEmitter接口,exit方法会在当进程退出的时候执行.因为进程退出之后将不再执行事件循 ...

  10. Git 系列之tag的用法---为你的代码标记版本号

    版权声明:本文为博主原创文章,未经博主允许不得转载.   目录(?)[-] 本地仓库操作 远程仓库操作 其他 tag 操作   在做app开发的时候经常有版本的概念,比如v1.0.v1.1之类的,不同 ...