目录
vue.js 代码实现
1. 步骤一
2. 步骤二
3. 步骤三
vue.JS 工作机制
初始化
编译
响应式
虚拟 dom
更新视图
编译
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 - 和 @开头的部分来设置响应式.
来源: https://www.cnblogs.com/dora-zc/p/10871026.html