源码学习:手写Mini-Vue
本文为笔者在学习vue源码过程中所作的笔记,剖析思路及代码实现来自于网课、网络文章和笔者自己的总结,笔者资历尚浅,写文章的目的在于加深理解,如何不妥之处,请在评论区友好地指出,讨论。
本文的内容仅可作为vue源码的入门,更详细深入的内容还得自己去看源码。本文要实现的内容可以参考官网来解释:
通过对 编译 、 挂载 、 更新的核心代码的实现(忽略一些边界判断),最终实现一个迷你版的Vue。
最终效果如下:
一、编译
具体的编译过程略写,这里只讲最简单的概念。
要理解这一部分的内容,首先得知道什么是虚拟DOM。
1.1 虚拟DOM
虚拟DOM其实就是一个js对象,我们所写的html代码最终会被转化为一个对象。而在Vue中,template模板的内容最终会被编译成一个js对象。该对象简洁地描述了html代码在页面上的显示效果。
注意:VDom(虚拟DOM)描述了一整个Dom树的结构,VNode(虚拟节点)描述的是其中的一个节点
那么为什么要用虚拟DOM呢,有什么优势?
1、提升渲染性能,真实DOM是十分复杂的,DOM的属性和方法来自于DOM规范,并由浏览器的JavaScript引擎实现。利用console.dir('DOm节点')
,我们可以方便的看到其上繁多的属性和方法。而虚拟DOM仅描述了你应用到的内容,其他内容暂时忽略,这样可以减少对真实DOM的频繁访问和操作,从而提升渲染性能
2、跨平台应用:虚拟DOM可以在不同平台和环境下进行渲染
3、方便patch新旧虚拟Dom,当我们将两个虚拟Dom进行对比时,省去了很多无关的内容,无疑提高了效率。
Vue提供了一个h函数,允许用它创建一个虚拟DOM对象
1.2H函数
我们所编写的template模板的内容,最终会被编译成一个render函数,render函数会返回h函数,而h函数创建了虚拟DOM。这里我们看一下h函数的实现
我们将h函数 进行简化,那么它的职责就是:返回一个VNode对象。下面看代码
虚拟节点:元素名(tag)、属性(props)、children(子节点)
javascript
//renderer.js
const h = (tag, props, children) => {
// 虚拟DOM就是一个js对象
return {
tag,
props,
children
}
}
在index.html中引入
xml
<body>
<div id="app"></div>
<script src="./renderer.js"></script>
<script>
// 1.通过h函数来创建一个vnode
const vnode = h('div', { class: 'ming' }, [
h("h2", null, "当前计数:100"),
h("button", null, "+1")
])
console.log(vnode);
</script>
</body>
打开控制台可以看到
事实上,我们上边创建的vnode已经可以看成是一个虚拟DOM了
xml
<div class="ming">
<h2>当前计数:100</h2>
<button>+1</button>
</div>
二、挂载
这一过程中,主要探索两个函数的实现,一个是mount 函数,一个是patch函数。
mount函数,用于将VNode转换为真实DOM,并挂载到页面上
patch函数 ,数据变化时生成新的VNode,用于对比两个VNode进行对比,并最终将变化的部分更新到页面上
2.1 mont函数
1、根据vnode创建出真实DOM,并处理相应的属性(如果有,则添加进新创建的节点中)
scss
//renderer.js
const mount = (vnode, container) => {
//将vnode -> 真实DOM
// 1.创建出真实的原生节点,并且在vnode上保留el
const el = vnode.el = document.createElement(vnode.tag)
// 2.处理props
//如果虚拟节点的props有值,则将值添加进新创建的节点上
if(vnode.props) {
for(const key in vnode.props) {
const value = vnode.props[key]
el.setAttribute(key, value)
}
}
}
2、属性props的边界处理
dom元素的属性可能是这样子的,针对这种情况,我们需要对事件进行监听
ini
<div onclick="function() {}"></div>
scss
const mount = (vnode, container) => {
//...
// 2.处理props
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key]
if (key.startsWith("on")) { //对事件进行监听
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
el.setAttribute(key, value)
}
}
}
}
3、处理childern
如果vnode没有子节点,那么就直接跳过
如果vnoed有子节点,那么分为两种情况:
- 1、子节点为文本字符串,直接将文本添加到新创建的真实的el元素上即可
- 2、如果子节点是一个数组,说明vnode的子节点有其他节点需要处理,递归调用mount函数即可
ini
// 3.处理childern
if (vnode.children) {
if (typeof vnode.children === "string") { //如果是文本
el.textContent = vnode.children
} else { //其他情况视为数组
vnode.children.forEach(item => {
mount(item, el)
})
}
}
4、将el挂载到container上
scss
//完整代码
const mount = (vnode, container) => {
//将vnode -> 真实DOM
// 1.创建出真实的原生,并且在vnode上保留el
const el = vnode.el = document.createElement(vnode.tag)
// 2.处理props
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key]
if (key.startsWith("on")) { //对事件进行监听
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
el.setAttribute(key, value)
}
}
}
// 3.处理childern
if (vnode.children) {
if (typeof vnode.children === "string") { //如果是文本
el.textContent = vnode.children
} else { //其他情况视为数组
vnode.children.forEach(item => {
mount(item, el)
})
}
}
// 4.将el挂载到container上
container.appendChild(el)
}
在index.html中使用
xml
<html>
<head>
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./renderer.js"></script>
<script>
// 1.通过h函数来创建一个vnode
const vnode = h('div', { class: 'ming' }, [
h("h2", null, "当前计数:100"),
h("button", null, "+1")
])
// 2. 通过mount函数,将vnode挂载到div#app上
mount(vnode, document.querySelector("#app"))
</script>
</body>
</html>
这样一来,页面上就可以显示出vnode的内容了
2.2 patch函数
假设用户更新了修改了vnode,这个时候我们需要进行differ算法
kotlin
const vnode = h('div', { class: 'ming' }, [
h("h2", null, "当前计数:100"),
h("button", null, "+1")
])
const vnode1 = h('div', null, '诶嘿')
例如:上边的代码中,如何通过differ算法,找到两者的不同之处,把新的修改内容替换掉原先vnode的内容,这一过程中尽可能复用旧vnode
1、判断两者的类型是否一致
scss
//renderer.js
const patch = (n1, n2) => {
if (n1.tag !== n2.tag) { //类型不同
//拿到n1的父节点(这里是<div id="app">)
const n1ElParent = n1.el.parentElement;
//删除n1
n1ElParent.removeChild(n1.el)
//将n2挂载上去
mount(n2, n1ElParent)
} else {
//...
}
}
如果两个类型不一致,则简单粗暴地将整个的n2替换掉n1(将n1的DOM树直接移除),而不是去修改n1
注意:这里的n1中的el先暂时忽略其来源,最终实现的时候我们会将该vnode的真实DOM存一份到其中
2、两者类型相同,处理属性
对比新旧两个虚拟DOM的属性,并将新的n2中的属性添加进el中
javascript
const patch = (n1, n2) => {
if (n1.tag !== n2.tag) {
//...
} else { //类型是相同的
// 1.取出element对象,并且在n2中进行保存
const el = n2.el = n1.el
// 2.处理props
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 2.1 获取所有的newProps,并添加进el中
for (const key in newProps) {
const newValue = newProps[key]
const oldValue = oldProps[key] //如果oldValue有值,说明新旧n1、n2存在相同的属性
if (newValue !== oldValue) {
//将不相同的属性进行添加操作
if (key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(), newValue)
} else {
el.setAttribute(key, newValue)
}
}
}
// 3.处理children
}
}
3、处理属性
剔除掉el的属性中,旧的虚拟DOM的属性
javascript
const patch = (n1, n2) => {
if (n1.tag !== n2.tag) {
//...
} else { //类型是相同的
// 1.取出element对象,并且在n2中进行保存
const el = n2.el = n1.el
// 2.处理props
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 2.1 获取所有的newProps,并添加进el中
//...
// 2.2 删除旧的props
for (const key in oldProps) {
if (key.startsWith("on")) { // 对事件监听的判断
const value = oldProps[key];
el.removeEventListener(key.slice(2).toLowerCase(), value)
}
if (!(key in newProps)) {
el.removeAttribute(key);
}
}
// 3.处理children
}
}
4、两者类型相同,处理children
如果新的节点是文本,直接替换掉旧节点的整个children即可
ini
// 3.处理children
const newChildren = n2.children || []
const oldChildren = n1.children || []
if (typeof newChildren === 'string') {
// 如果新节点的children是文本,替换掉旧el的整个innerHTML
el.innerHTML = newChildren
}
5、处理children
新节点是本身是一个数组的情况:假设旧节点是一个文本,那么我们应该先清空旧节点的文本,并将新节点的children挂载到对应的位置上
ini
// 3.处理children
const newChildren = n2.children || []
const oldChildren = n1.children || []
// 3.1 新节点是一个文本
if (typeof newChildren === 'string') {
// 替换掉旧el的整个innerHTML
el.innerHTML = newChildren
} else {//3.2 新节点是一个数组
//旧节点是一个文本
if (typeof oldChildren === 'string') {
el.innerHTML ="" //清空
newChildren.forEach(item => {
mount(item, el) // 递归 :将每一个item挂载到el上
})
}
6、处理children
新节点是一个数组,当旧节点也是一个数组时,就需要分为多种情况进行考虑
取出新旧节点的children的长度最小值,以最小值的长度为遍历依据,使新旧节点两两进行对比(patch,回调)
(考虑到新旧节点都是可能有多层的树结构,子节点,子节点的子节点....)
如果新节点的children有多余的元素,或者旧节点的children有多余的元素,就要进行添加/删除的操作
ini
// 3.处理children
const newChildren = n2.children || []
const oldChildren = n1.children || []
// 3.1 新节点是一个文本
if (typeof newChildren === 'string') {
// 替换掉旧el的整个innerHTML
el.innerHTML = newChildren
} else {//3.2 新节点是一个数组
//3.2.1 旧节点是一个文本
if (typeof oldChildren === 'string') {
el.innerHTML ="" //清空
newChildren.forEach(item => {
mount(item, el) // 递归 :将每一个item挂载到el上
})
} else {
//3.2.2旧节点是一个数组
//oldChildren: [v1, v2, v3]
//newChildren: [v1, v5, v6]
const commonLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < commonLength; i++) {
// 有相同节点的元素进行patch操作
patch(oldChildren[i], newChildren[i])
}
}
}
7、处理children
当新节点的children长度大于旧节点的children,就需要对超出公共长度的节点进行添加操作
scss
else {
//3.2.2旧节点是一个数组
//oldChildren: [v1, v2, v3]
//newChildren: [v1, v5, v6]
const commonLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < commonLength; i++) {
// 有相同节点的元素进行patch操作
patch(oldChildren[i], newChildren[i])
}
//newChildren.length > oldChildren.length
//oldChildren: [v1, v2, v3]
//newChildren: [v1, v5, v6, v7, v8]
if (newChildren.length > oldChildren.length) {
//newChildren: [v7, v8] 添加至el上
newChildren.slice(oldChildren.length).forEach(item => {
mount(item, el)
})
}
}
8、处理children
当新节点的children的长度小于旧节点的children的长度,我们需要将旧节点大于公共长度的的元素进行移除
scss
else {
//3.2.2旧节点是一个数组
//情况一
//oldChildren: [v1, v2, v3]
//newChildren: [v1, v5, v6]
const commonLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < commonLength; i++) {
// 有相同节点的元素进行patch操作
patch(oldChildren[i], newChildren[i])
}
////情况二:newChildren.length > oldChildren.length
//oldChildren: [v1, v2, v3]
//newChildren: [v1, v5, v6, v7, v8]
if (newChildren.length > oldChildren.length) {
//newChildren: [v7, v8] 添加至el上
newChildren.slice(oldChildren.length).forEach(item => {
mount(item, el)
})
}
// 情况三:newChildren.length < oldChildren.length
//oldChildren: [v1, v2, v3, v7, v8]
//newChildren: [v1, v5, v6]
if (newChildren.length < oldChildren.length) {
//oldChildren: [v7, v8]进行移除
oldChildren.slice(newChildren.length).forEach(item => {
el.removeChild(item.el)
})
}
}
patch算是写好了,但是暂时调不通。创建并传入的vnode,并没有n1这个属性(mount时注入)
三、更新
数据动态更新这一部分就涉及到vue的一个重要概念:响应式原理。这里将重点介绍vue2和vue3响应式实现的过程
响应式的思想
什么是响应式:1、数据发生了变化,依赖于该数据的函数再次发生调用
xml
<script>
const info = { counter: 100 }
function dobuleCounter() {
console.log(info.counter * 2) //依赖
}
dobuleCounter()
//当页面某处执行了
info.counter++
//应该自动在这里再执行一次dobuleCounter()
</script>
dobuleCounter
函数依赖于info.counter
,当info.counter
发生改变时,dobuleCounter
函数应该使用新的数据执行一次
说到底,当某一个数据发生变化时,页面中的另外一个数据对这个数据有依赖,也应该进行相应的变化
3.1 依赖收集系统
创建一个收集依赖的类
Dep: depend(依赖) subscribers:订阅者
kotlin
class Dep {
constructor () {
this.subscribers = [] //用于收集依赖
}
}
当然,this.subscribers = []
使用集合Set会更好,因为集合规定了元素不能出现重复。重复了只保留一个
javascript
class Dep {
constructor () {
this.subscribers = new Set() //用于收集依赖
}
//收集依赖,修改了数据后会产生的影响
addEffect(effect) {
//数据变化后,将这个被影响的目标添加进subscribers中
this.subscribers.add(effect)
}
//执行
notify() {
this.subscribers.forEach(effect => {
//调用被影响的目标
effect()
})
}
}
// 创建实例
const dep = new Dep()
const info = {
counter: 100
}
//下面的两个函数依赖于info.counter
function dobuleCounter() {
console.log(info.counter * 2)
}
function powerCounter() {
console.log(info.counter * info.counter)
}
//收集依赖
dep.addEffect(dobuleCounter)
dep.addEffect(powerCounter)
//依赖发生改变
info.counter++
dep.notify()
在index.html在引入上边的代码,可以看到代码确实info.counter
改变后,对应的dobuleCounter、powerCounter
也重新调用并更新了数据
问题:
上边的代码存在很大的问题,比如需要手动收集依赖、需要手动执行执行依赖的函数。
我们希望当一个对象对目标有依赖时,能够自动被收集,自动在目标改变时,自动执行
对上边的代码进行重新构建
javascript
class Dep {
constructor () {
this.subscribers = new Set() //用于收集依赖
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
this.subscribers.forEach(effect => {
//调用被影响的目标
effect()
})
}
}
let activeEffect = null
function watchEffect(effect) {
activeEffect = effect
dep.depend()
effect() //执行时,因为读取数据而被收集了依赖
activeEffect = null
}
// 创建实例
const dep = new Dep()
const info = {
counter: 100
}
//下面的两个函数依赖于info.counter
watchEffect(function() {
console.log(info.counter * 2)
})
watchEffect(function() {
console.log(info.counter * info.counter)
})
//数据发生改变
info.counter++
dep.notify()
先理解上边的代码,不然下面的代码可能理解不了
上边的代码依旧有些缺陷
假设存在effect1、effect2、effect3,其中effect1、effect3依赖于info中的name属性,当只有name属性发生变化时,再次调用dep.nitify()
显然是不合适的(因为,effect2并没有受影响)
javascript
//实例
const dep = new Dep()
//数据
const info = {
name: 'linming',
counter: 10
}
const foo = {
height: 1.88
}
//effect1
watchEffect(function () {
console.log(info.counter * 2, info.name)
} )
//effect2
watchEffect(function () {
console.log(info.counter * info.counter)
})
//effect3
watchEffect(function () {
console.log(info.counter + 10, info.name)
})
info.name = "linlin"
dep.notify() //不适合
所以,我们不能随随便便地进行依赖收集。
也不能将所有的依赖都收集在一个dep中,而是应该不同的数据,应该创建不同的dep去收集。
> 思路:每一个属性,都应当有专门一个subscribers来收集其依赖
scss
dep1(info.counter)=》subscribers //关于info.counter的订阅者
dep2(info.name)=》subscribers //关于info.name的订阅者
dep3(foo.height)=》subscribers //关于foo.height的订阅者
需要一种专门的数据结构来管理这些dep实例------MAP(可以将对象作为键),当然使用Weakmap会更好。
简单说明map和weakmap的区别
1、map与weakmap是两种不同的数据结构,都能以引用数据类型作为键,不同的是weakmap只能以引用类型作为键
2、weakmap的键是弱引用,而map是强引用。这也是vue响应式采用weakmap的原因
弱引用和强引用。
ini//用一个例子来说明 const obj = { xxx: 'xxx' } const map = { key: obj }, const wmap = { key: obj } obj = null
将obj置为null时,按理来说map引用着它,那么它不会被垃圾回收掉。但是如果是wmap,当obj为null时,weakmap会认为这是无效的引用,直接就将key所指向的地址垃圾回收掉。有效地防止了内存泄漏
3.2 vue2数据劫持
实现效果
封装一个reactive函数,希望实现以下效果
php
//传入一个对象,将其变为响应式
const info = reactive({ name: 'linming', counter: 10 })
const foo = reactive({ height: 1.88 })
最重要的是,我们需要实现当info、foo
对象里边的值发生变化时,能够进行数据劫持
在vue2中,数据响应原理使用了Object.defineProperty
,而vue3使用了proxy
1、实现数据劫持
这里我们先写vue2的数据劫持方式
javascript
// vue2的数据劫持
function reactive(raw) {
Object.keys(raw).forEach(key => {
Object.defineProperty(raw, key, {
get() { },
set(newValue) { }
})
})
return raw
}
const info = reactive({ name: 'linming', counter: 10 })
const foo = reactive({ height: 1.88 })
这样一来,我们就实现了对info对象、foo对象的数据劫持
我们可以在get里边实现依赖的收集
javascript
function reactive(raw) {
const dep = new Dep()
Object.keys(raw).forEach(key => {
Object.defineProperty(raw, key, {
get() {
dep.depend()
},
set(newValue) { }
})
})
return raw
}
这样一来,一旦数据发生改变,例如counter
,与counter相关的函数目标就会被添加依赖中
封装getDep函数
上边的代码中,每一次添加依赖,都是新创建一个dep实例,显然是不合适的
所以,我们需要封装一个这个工具函数,基本机构如下
arduino
// 结构图:
//1.最外层的targetMap(WeakMap类型,键为对象)
//2.targetMap的属性为:target(键,对象):depsMap(值,对象)
//3.depsMap对象的属性有:key(键,字符串):dep(值,对象)
scss
const targetMap = new WeakMap()
function getDep(target, key) {
// 1.根据对象(target)取出对应的map对象
let depsMap = targetMap.get(target)
//如果没有则创建
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 2. 取出具体的dep对象
let dep = depsMap.get(key)
//如果没有则创建
if(!dep) {
dep = new Dep()
depsMap.set(key, dep)
}
return dep
}
调用getDep函数,最终实现效果如下
javascript
let activeEffect = null
function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}
//创建getDate工具函数
const targetMap = new WeakMap()
function getDep(target, key) {
// 1.根据对象(target)取出对应的map对象
let depsMap = targetMap.get(target)
//如果没有则创建
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 2. 取出具体的dep对象
let dep = depsMap.get(key)
//如果没有则创建
if(!dep) {
dep = new Dep()
depsMap.set(key, dep)
}
return dep
}
// vue2的数据劫持
function reactive(raw) {
Object.keys(raw).forEach(key => {
//创建dep实例
const dep = getDep(raw, key)
let value = raw[key]
Object.defineProperty(raw, key, {
get() {
// 获取值时
dep.depend()
return value
},
set(newValue) {
// 设置值时
value = newValue
dep.notify() //响应
}
})
})
return raw
}
// 创建实例
const dep = new Dep()
//测试代码
const info = reactive({ name: 'linming', counter: 10 })
const foo = reactive({ height: 1.88 })
//effect1
watchEffect(function () {
console.log("effect1:", info.counter * 2, info.name)
} )
//effect2
watchEffect(function () {
console.log("effect2:", info.counter * info.counter)
})
//effect3
watchEffect(function () {
console.log("effect3:", info.counter + 10, info.name)
})
//effect4
watchEffect(function () {
console.log("effect4:", foo.height);
})
//数据发生改变
info.counter++
当info.counter++
发生变化时,结果如下
arduino
//effect1: 20 linming
//effect2: 100
//effect3: 20 linming
//effect4: 1.88
//effect1: 22 linming
//effect2: 121
//effect3: 21 linming
前四个是收集依赖,必然会执行。后边的effect1、2、3因为依赖了info.counter
,所以也执行了,而effect4并没有依赖它,就没有执行
3.3 vue3数据劫持
为什么vue3选择Proxy?两者区别
1、主要的原因:Object.defineProperty是劫持对象的属性,如果新增元素,那么vue2需要再次调用definedProperty。而Proxy劫持的是整个对象,不需要做特殊处理
2、修改对象的不同:使用Object.defineProperty时,修改原来的obj对象就可以触发拦截;而使用proxy,就必须修改代理对象,即Proxy的实例才可以触发拦截
3、Proxy能观察的类型比defineProperty更丰富
具体代码如下
scss
// vue3的数据劫持
function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const dep = getDep(target, key)
dep.depend()
return target[key]
},
set(target, key, newValue) {
const dep = getDep(target, key)
target[key] = newValue
dep.notify()
}
})
}
四、实现
4.1 mini-vue实现
1、创建一个index.html文件,导入renderer函数和reactive函数
并创建一个根组件
xml
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./引入上边所实现的的代码"></script>
<script>
// 1.创建根组件
const app = {
data: reactive({
counter : 100
}),
render() { //返回一个vnode
return h("div", null, [
h("h2", null, `当前计数:${ this.data.counter }`),
h("button", {
onclick: () =>{
this.data.counter++
}
}, "+1")
])
}
}
//挂载根组件
createApp(App).mount("#app")
//或者这样写
const app = createApp(App)
app.mount("#app")
</script>
</body>
</html>
2、创建createApp函数
第一次进入该函数是挂载根组件,第二次是更新
javascript
function createApp(rootComponent) {
return {
mount(selector) {
const container = document.querySelector(selector)
let isMounted = false //是否完成挂载
let oldVnode = null
//注:传入的函数我们将其称为effect函数,方便理解
watchEffect(function() { //挂载时,将进行依赖的收集
if (!isMounted) {
//尚未挂载
oldVnode = rootComponent.render()
mount(oldVnode, container)
isMounted = true //将其转态改为已挂载
} else {
//已实现挂载
const newVnode = rootComponent.render()
//更新数据
patch(oldVnode, newVnode)
oldVnode = newVnode //为了可再次更新
}
})
}
}
}
运行流程
1、调用createApp(App)
,返回vue实例,该实例包含了mount方法
2、调用app.mount("#app")
,会执行watchEffect,并将传入的函数执行一遍。第一次调用必定是未挂载状态,则执行render()函数形成虚拟Dom
注意:在执行render过程中,访问了data.counter(因为我们counter已事先被reactive劫持),所以此处将形成
counter ===> [effect]
的订阅关系
3、虚拟Dom形成后将调用mount(oldVnode, container)
挂载到页面,至此页面初次渲染完毕
更新流程
1、当counter
发生更新,触发get操作,便会更新counter引用了counter的依赖,即此处会调用effect函数
2、第二次调用effect函数组件已挂载,patch(oldVnode, newVnode)
对比新旧数据,并最终将变化更新到页面上
建议通过debugger一步步看整体的代码运行过程,可以比较清晰的知道各个方法变量之间的联系
本文是从自己的学习笔记中整理出来,有很多内容不够严谨,但是大体的思路是对的。如果有不对的地方,请谅解。