1.vue的diff算法
Diff 作用
Diff 的出现,就会为了减少更新量,找到最小差异部分DOM,只更新差异部分DOM就好了,这样消耗就会小一些,数据变化一下,没必要把其他没有涉及的没有变化的DOM 也替换了。
Diff 做法
Vue 只会对新旧节点中父节点是相同节点的那一层子节点进行比较,也可以说成是,只有两个新旧节点是相同节点的时候,才会去比较他们各自的子节点。
最大的根节点一开始可以直接比较,这也叫做同层级比较,并不需要递归,虽然好像降低了一些复用性,也是为了避免过度优化,是一种很高效的 Diff 算法。
新旧节点是什么
所有的新旧节点指的都是 Vnode 节点,Vue 只会比较 Vnode 节点,而不是比较 DOM。因为 Vnode 是 JS 对象,不受平台限制,所以以它作为比较基础,代码逻辑后期不需要改动,拿到比较结果后,根据不同平台调用相应的方法进行处理就好了。
父节点是相同节点是什么意思?
比如下图出现的四次比较(从 first 到 fouth),他们的共同特点都是有相同的父节点
比如蓝色方的比较,新旧子节点的父节点是相同节点 1
比如红色方的比较,新旧子节点的父节点都是 2
所以他们才有比较的机会
而下图中,只有两次比较,就是因为在蓝色方比较中,并没有相同节点,所以不会再进行下级子节点比较
Diff 比较逻辑
Diff 比较的内核是 节点复用,所以 Diff 比较就是为了在新旧节点中找到相同的节点 ,这个的比较逻辑是建立在上一步说过的同层比较基础之上的。所以说,节点复用,找到相同节点并不是无限制递归查找。
比如下图中,的确旧节点树和新节点树中有相同节点 6,但是然并卵,旧节点6并不会被复用。
就算在同一层级,然而父节点不一样,依旧然并卵
只有这种情况的节点会被复用,相同父节点 8
Diff 的比较逻辑
(1)能不移动,尽量不移动
(2)没得办法,只好移动
(3)实在不行,新建或删除
比较处理流程是下面这样
在新旧节点中
(1)先找到不需要移动的相同节点,消耗最小
(2)再找相同但是需要移动的节点,消耗第二小
(3)最后找不到,才会去新建删除节点,保底处理
比较是为了修改DOM 树
其实这里存在三种树,一个是页面DOM 树,一个是旧VNode 树,一个是新Vnode 树。页面DOM 树和旧VNode 树节点一一对应的,而新Vnode 树则是表示更新后页面DOM 树该有的样子。这里把旧Vnode 树和新Vnode树进行比较的过程中,不会对这两棵Vode树进行修改,而是以比较的结果直接对真实DOM 进行修改。
比如说,在旧 Vnode 树同一层中,找到和新Vnode 树中一样但位置不一样节点,此时需要移动这个节点,但是不是移动旧 Vnode 树中的节点,而是直接移动DOM。
总的来说,新旧 Vnode 树是拿来比较的,页面DOM树是拿来根据比较结果修改的。
Diff 简单例子
比如下图存在这两棵需要比较的新旧节点树和一棵需要修改的页面 DOM树
第一轮比较开始
因为父节点都是 1,所以开始比较他们的子节点,按照我们上面的比较逻辑,所以先找相同 && 不需移动 的点,毫无疑问,找到 2。
拿到比较结果,这里不用修改DOM,所以 DOM 保留在原地
第二轮比较开始
然后,没有相同 &&不需移动 的节点 了,只能第二个方案,开始找相同的点,找到节点5,相同但是位置不同,所以需要移动。
拿到比较结果,页面DOM树需要移动DOM了,不修改,原样移动
第三轮比较开始
相同节点也没得了,没得办法了,只能创建了,所以要根据新Vnode 中没找到的节点去创建并且插入,然后旧Vnode 中有些节点不存在新VNode 中,所以要删除。
于是开始创建节点 6 和 9,并且删除节点 4 和 5
然后页面就完成更新啦。
新建实例 到 开始Diff 的流程
首先,当你新建实例的时候,比如这样
你调用一个 Vue 函数,所以来看下 Vue 函数
function Vue() {
... 已省略其他
new Watcher(function() {
vm._update(vm._render());
})
... 已省略其他
}
函数中做了两件事
(1)为实例新建一个 watcher
(2)为 watcher 绑定更新回调(就是 new Watcher 传入的 function )
每个实例都会有一个专属的 watcher,而绑定的回调,在页面更新时会调用。
我们现在来看下简化的 Watcher 的源码
funciton Watcher(expOrFn){
this.getter = expOrFn;
this.get();
}
Watcher.prototype.get = function () {
this.getter()
}
watcher 会保存更新回调,并且在新建 watcher 的时候就会立刻调用一遍更新回调
现在我们继续看 更新回调的内容
vm._update(vm._render());
vm._render
生成页面模板对应的 Vnode 树,比如
生成的 Vnode 树是( 其中num的值是111 )
{
tag: "div",
children:[{
tag: "span"
},{
tag: undefined,
text: "111"
}]
}
vm._update
比较 旧Vnode 树和 vm._render 生成的新 Vnode 树进行比较
比较完后,更新页面的DOM,从而完成更新
Vue.prototype._update = function(vnode) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
vm._vnode = vnode;
// 不存在旧节点
if (!prevVnode) {
vm.$el = vm.__patch__(
vm.$el, vnode,
vm.$options._parentElm,
vm.$options._refElm
);
}
else {
vm.$el = vm.__patch__(
prevVnode, vnode
);
}
};
解释其中几个点
vm._vnode
这个属性保存的就是当前 Vnode 树,当页面开始更新,而生成了新的 Vnode 树之后,这个属性则会替换成新的Vnode.所以保存在这里,是为了方便拿到旧 Vnode 树.
vm.patch
var patch = createPatchFunction();
Vue.prototype.__patch__ = patch ;
是经过一个 createPatchFunciton 生成的,然后赋值到 Vue 的原型上,所以可以 vm.__patch__
调用。
createPatchFunction
function createPatchFunction() {
return function patch(
oldVnode, vnode, parentElm, refElm
) {
// 没有旧节点,直接生成新节点
if (!oldVnode) {
createElm(vnode, parentElm, refElm);
}
else {
// 且是一样 Vnode
if (sameVnode(oldVnode, vnode)) {
// 比较存在的根节点
patchVnode(oldVnode, vnode);
}
else {
// 替换存在的元素
var oldElm = oldVnode.elm;
var _parentElm = oldElm.parentNode
// 创建新节点
createElm(vnode, _parentElm, oldElm.nextSibling);
// 销毁旧节点
if (_parentElm) {
removeVnodes([oldVnode], 0, 0);
}
}
}
return vnode.elm
}
}
这个函数的作用就是,比较新节点和旧节点有什么不同,然后完成更新,所以你看到接收一个 oldVnode 和 vnode,处理的流程分为
(1)没有旧节点
没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全部都是新建,所以只调用 createElm。
(2)旧节点和新节点自身一样(不包括其子节点)
通过 sameVnode 判断节点是否一样,旧节点和新节点自身一样时,直接调用 patchVnode 去处理这两个节点。
当两个Vnode自身一样的时候,我们需要做什么?
首先,自身一样,我们可以先简单理解,是 Vnode 的两个属性 tag 和 key 一样。那么,我们是不知道其子节点是否一样的,所以肯定需要比较子节点。所以,patchVnode 其中的一个作用,就是比较子节点
(3)旧节点和新节点自身不一样
当两个节点不一样的时候,不难理解,直接创建新节点,删除旧节点。
patchVnode
function patchVnode(oldVnode, vnode) {
if (oldVnode === vnode) return
var elm = vnode.elm = oldVnode.elm;
var oldCh = oldVnode.children;
var ch = vnode.children;
// 更新children
if (!vnode.text) {
// 存在 oldCh 和 ch 时
if (oldCh && ch) {
if (oldCh !== ch)
updateChildren(elm, oldCh, ch);
}
// 存在 newCh 时,oldCh 只能是不存在,如果存在,就跳到上面的条件了
else if (ch) {
if (oldVnode.text) elm.textContent = '';
for (var i = 0; i <= ch.length - 1; ++i) {
createElm(
ch[i],elm, null
);
}
}
else if (oldCh) {
for (var i = 0; i<= oldCh.length - 1; ++i) {
oldCh[i].parentNode.removeChild(el);
}
}
else if (oldVnode.text) {
elm.textContent = '';
}
}
else if (oldVnode.text !== vnode.text) {
elm.textContent = vnode.text;
}
}
正如我们所想,这个函数的确会去比较处理子节点。总的来说,这个函数的作用是
(1)Vnode 是文本节点,则更新文本(文本节点不存在子节点)
当 VNode 存在 text 这个属性的时候,就证明了 Vnode 是文本节点。我们可以先来看看 文本类型的 Vnode 是什么样子
所以当 Vnode 是文本节点的时候,需要做的就是,更新文本,同样有两种处理
1)当新Vnode.text 存在,而且和旧 VNode.text 不一样时
直接更新这个 DOM 的文本内容
elm.textContent = vnode.text;
注:textContent 是 真实DOM 的一个属性, 保存的是 dom 的文本,所以直接更新这个属性
2)新Vnode 的 text 为空,直接把文本DOM 赋值给空
elm.textContent = '';
(2)Vnode 有子节点,则处理比较更新子节点
当 Vnode 存在子节点的时候,因为不知道新旧节点的子节点是否一样,所以需要比较,才能完成更新,这里有三种处理
1)新旧节点都有子节点,而且不一样
又出现了一个新函数,那就是 updateChildren,这个函数非常的重要,是 Diff 的核心模块,蕴含着 Diff 的思想。
我们先来思考下 updateChildren 的作用。记得条件,当新节点和旧节点都存在,要怎么去比较才能知道有什么不一样呢?使用遍历,新子节点和旧子节点一个个比较。如果一样,就不更新,如果不一样,就更新。
2)只有新节点
只有新节点,不存在旧节点,那么没得比较了,所有节点都是全新的,所以直接全部新建就好了,新建是指创建出所有新DOM,并且添加进父节点的。
3)只有旧节点
只有旧节点而没有新节点,说明更新后的页面,旧节点全部都不见了。那么要做的,就是把所有的旧节点删除,也就是直接把DOM 删除。
updateChildren
function updateChildren(parentElm, oldCh, newCh) {
var oldStartIdx = 0;
var oldEndIdx = oldCh.length - 1;
var oldStartVnode = oldCh[0];
var oldEndVnode = oldCh[oldEndIdx];
var newStartIdx = 0;
var newEndIdx = newCh.length - 1;
var newStartVnode = newCh[0];
var newEndVnode = newCh[newEndIdx];
var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
// 不断地更新 OldIndex 和 OldVnode ,newIndex 和 newVnode
while (
oldStartIdx <= oldEndIdx &&
newStartIdx <= newEndIdx
) {
if (!oldStartVnode) {
oldStartVnode = oldCh[++oldStartIdx];
}
else if (!oldEndVnode) {
oldEndVnode = oldCh[--oldEndIdx];
}
// 旧头 和新头 比较
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
}
// 旧尾 和新尾 比较
else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
}
// 旧头 和 新尾 比较
else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode);
// oldStartVnode 放到 oldEndVnode 后面,还要找到 oldEndValue 后面的节点
parentElm.insertBefore(
oldStartVnode.elm,
oldEndVnode.elm.nextSibling
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
}
// 旧尾 和新头 比较
else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode);
// oldEndVnode 放到 oldStartVnode 前面
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
}
// 单个新子节点 在 旧子节点数组中 查找位置
else {
// oldKeyToIdx 是一个 把 Vnode 的 key 和 index 转换的 map
if (!oldKeyToIdx) {
oldKeyToIdx = createKeyToOldIdx(
oldCh, oldStartIdx, oldEndIdx
);
}
// 使用 newStartVnode 去 OldMap 中寻找 相同节点,默认key存在
idxInOld = oldKeyToIdx[newStartVnode.key]
// 新孩子中,存在一个新节点,老节点中没有,需要新建
if (!idxInOld) {
// 把 newStartVnode 插入 oldStartVnode 的前面
createElm(
newStartVnode,
parentElm,
oldStartVnode.elm
);
}
else {
// 找到 oldCh 中 和 newStartVnode 一样的节点
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode);
// 删除这个 index
oldCh[idxInOld] = undefined;
// 把 vnodeToMove 移动到 oldStartVnode 前面
parentElm.insertBefore(
vnodeToMove.elm,
oldStartVnode.elm
);
}
// 只能创建一个新节点插入到 parentElm 的子节点中
else {
// same key but different element. treat as new element
createElm(
newStartVnode,
parentElm,
oldStartVnode.elm
);
}
}
// 这个新子节点更新完毕,更新 newStartIdx,开始比较下一个
newStartVnode = newCh[++newStartIdx];
}
}
// 处理剩下的节点
if (oldStartIdx > oldEndIdx) {
var newEnd = newCh[newEndIdx + 1]
refElm = newEnd ? newEnd.elm :null;
for (; newStartIdx <= newEndIdx; ++newStartIdx) {
createElm(
newCh[newStartIdx], parentElm, refElm
);
}
}
// 说明新节点比对完了,老节点可能还有,需要删除剩余的老节点
else if (newStartIdx > newEndIdx) {
for (; oldStartIdx<=oldEndIdx; ++oldStartIdx) {
oldCh[oldStartIdx].parentNode.removeChild(el);
}
}
}
首先要明确这个函数处理的是什么
处理的是新子节点和旧子节点,循环遍历逐个比较
如何循环遍历?
(1)使用 while
(2)新旧节点数组都配置首尾两个索引
新节点的两个索引:newStartIdx , newEndIdx
旧节点的两个索引:oldStartIdx,oldEndIdx
以两边向中间包围的形式来进行遍历
头部的子节点比较完毕,startIdx 就加1
尾部的子节点比较完毕,endIdex 就减1
只要其中一个数组遍历完(startIdx<endIdx),则结束遍历
源码处理的流程分为两个
(1)比较新旧子节点
注:这里有两个数组,一个是新子Vnode数组,一个旧子Vnode数组。在比较过程中,不会对两个数组进行改变(比如不会插入,不会删除其子项),而所有比较过程中都是直接插入删除 真实页面DOM。
我们明确一点,比较的目的是什么?
找到 新旧子节点中的相同的子节点,尽量以移动替代新建去更新DOM,只有在实在不同的情况下,才会新建。
比较更新计划步骤
首先考虑,不移动DOM;其次考虑,移动DOM;最后考虑,新建 / 删除 DOM。能不移动,尽量不移动。不行就移动,实在不行就新建。
下面开始说源码中的比较逻辑:
五种比较逻辑如下
1)旧头 == 新头
sameVnode(oldStartVnode, newStartVnode)
当两个新旧的两个头一样的时候,并不用做什么处理,符合我们的步骤第一条,不移动DOM完成更新。但是看到一句,patchVnode,就是为了继续处理这两个相同节点的子节点,或者更新文本。因为我们不考虑多层DOM 结构,所以 新旧两个头一样的话,这里就算结束了,可以直接进行下一轮循环。
newStartIdx ++ , oldStartIdx ++
2)旧尾 == 新尾
sameVnode(oldEndVnode, newEndVnode)
和头头相同的处理是一样的,尾尾相同,直接跳入下个循环。
newEndIdx ++ , oldEndIdx ++
3)旧头 == 新尾
sameVnode(oldStartVnode, newEndVnode)
这步不符合不移动DOM,所以只能移动DOM 了。
源码是这样的
parentElm.insertBefore(
oldStartVnode.elm,
oldEndVnode.elm.nextSibling
);
以新子节点的位置来移动的,旧头在新子节点的末尾,所以把 oldStartVnode 的 dom 放到 oldEndVnode 的后面。但是因为没有把dom 放到谁后面的方法,所以只能使用 insertBefore
,即放在 oldEndVnode 后一个节点的前面。
图示是这样的
然后更新两个索引
oldStartIdx++,newEndIdx--
4)旧尾 == 新头
sameVnode(oldEndVnode, newStartVnode)
同样不符合不移动DOM,也只能移动DOM 了
parentElm.insertBefore(
oldEndVnode.elm,
oldStartVnode.elm
);
把 oldEndVnode DOM 直接放到当前 oldStartVnode.elm 的前面。
图示是这样的
然后更新两个索引
oldEndIdx--,newStartIdx++
5)单个查找
当前面四种比较逻辑都不行的时候,这是最后一种处理方法。拿新子节点的子项,直接去旧子节点数组中遍历,找一样的节点出来。
流程大概是
1、生成旧子节点数组以 vnode.key 为key 的 map 表
这个map 表的作用,就主要是判断存在什么旧子节点,比如你的旧子节点数组是
[{
tag:"div", key:1
},{
tag:"strong", key:2
},{
tag:"span", key:4
}]
经过 createKeyToOldIdx 生成一个 map 表 oldKeyToIdx,{ vnodeKey: 数组Index },属性名是 vnode.key,属性值是该 vnode 在children 的位置是这样
oldKeyToIdx = {
1:0,
2:1,
4:2
}
2、拿到新子节点数组中一个子项,判断它的key是否在上面的map 中
拿到新子节点中的 子项Vnode,然后拿到它的 key,去匹配map 表,判断是否有相同节点。
oldKeyToIdx[newStartVnode.key]
3、不存在,则新建DOM
直接创建DOM,并插入oldStartVnode 前面。
createElm(newStartVnode, parentElm, oldStartVnode.elm);
4、存在,继续判断是否 sameVnode
找到这个旧子节点,然后判断和新子节点是否 sameVnode,如果相同,直接移动到 oldStartVnode 前面,如果不同,直接创建插入 oldStartVnode 前面。
我们上面说了比较子节点的处理的流程分为两个
①比较新旧子节点
②比较完毕,处理剩下的节点
(2)比较完毕,处理剩下的节点
在updateChildren 中,比较完新旧两个数组之后,可能某个数组会剩下部分节点没有被处理过,所以这里需要统一处理
1 新子节点遍历完了
newStartIdx > newEndIdx
新子节点遍历完毕,旧子节点可能还有剩,所以我们要对可能剩下的旧节点进行批量删除!
就是遍历剩下的节点,逐个删除DOM。
for (; oldStartIdx <= oldEndIdx; ++oldStartIdx) {
oldCh[oldStartIdx]
.parentNode
.removeChild(el);
}
2旧子节点遍历完了
oldStartIdx > oldEndIdx
旧子节点遍历完毕,新子节点可能有剩,所以要对剩余的新子节点处理。很明显,剩余的新子节点不存在旧子节点中,所以全部新建。
for (; newStartIdx <= newEndIdx; ++newStartIdx) {
createElm(
newCh[newStartIdx],
parentElm,
refElm
);
}
但是新建有一个问题,就是插在哪里?
所以其中的 refElm 就成了疑点,看下源码
var newEnd = newCh[newEndIdx + 1]
refElm = newEnd ? newEnd.elm :null;
refElm 获取的是 newEndIdx 后一位的节点,当前没有处理的节点是 newEndIdx。也就是说 newEndIdx+1 的节点如果存在的话,肯定被处理过了。如果 newEndIdx 没有移动过,一直是最后一位,那么就不存在 newChnewEndIdx + 1。那么 refElm 就是空,那么剩余的新节点就全部添加进 父节点孩子的末尾,相当于
for (; newStartIdx <= newEndIdx; ++newStartIdx) {
parentElm.appendChild(
newCh[newStartIdx]
);
}
如果 newEndIdx 移动过,那么就逐个添加在 refElm 的前面,相当于
for (; newStartIdx <= newEndIdx; ++newStartIdx) {
parentElm.insertBefore(
newCh[newStartIdx] ,
refElm
);
}
如图
走流程
以下的节点,绿色表示未处理,灰色表示已经处理,淡绿色表示正在处理,红色表示新插入,如下
现在Vue 需要更新,存在下面两组新旧子节点,需要进行比较,来判断需要更新哪些节点
1头头比较,节点一样,不需移动,只用更新索引
更新索引,newStartIdx++ , oldStartIdx++
开始下轮处理
一系列判断之后,【旧头 2】 和 【 新尾 2】相同,直接移动到 oldEndVnode 后面
更新索引,newEndIdx-- ,oldStartIdx ++
开始下轮处理
3一系列判断之后,【旧头 2】 和 【 新尾 2】相同,直接移动到 oldStartVnode 前面
更新索引,oldEndIdx-- ,newStartIdx++
开始下轮比较
4只剩一个节点,走到最后一个判断,单个查找
找不到一样的,直接创建插入到 oldStartVnode 前面
更新索引,newStartIdx++
此时 newStartIdx> newEndIdx ,结束循环
5 批量删除可能剩下的老节点
此时看 旧 Vnode 数组中, oldStartIdx 和 oldEndIdx 都指向同一个节点,所以只用删除 oldVnode-4 这个节点。
ok,完成所有比较流程。
参考:【Vue原理】Diff - 白话版
【Vue原理】Diff - 源码版 之 从新建实例到开始diff
【Vue原理】Diff - 源码版 之 Diff 流程
2.Javascript十大常用设计模式
理解工厂模式
工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。
简单的工厂模式可以理解为解决多个相似的问题;这也是她的优点;比如如下代码:
function CreatePerson(name,age,sex) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.sex = sex;
obj.sayName = function(){
return this.name;
}
return obj;
}
var p1 = new CreatePerson("longen",'28','男');
var p2 = new CreatePerson("tugenhua",'27','女');
console.log(p1.name); // longen
console.log(p1.age); // 28
console.log(p1.sex); // 男
console.log(p1.sayName()); // longen
console.log(p2.name); // tugenhua
console.log(p2.age); // 27
console.log(p2.sex); // 女
console.log(p2.sayName()); // tugenhua
// 返回都是object 无法识别对象的类型 不知道他们是哪个对象的实列
console.log(typeof p1); // object
console.log(typeof p2); // object
console.log(p1 instanceof Object); // true
如上代码:函数CreatePerson能接受三个参数name,age,sex等参数,可以无数次调用这个函数,每次返回都会包含三个属性和一个方法的对象。
工厂模式是为了解决多个类似对象声明的问题;也就是为了解决实列化对象产生重复的问题。
优点:能解决多个相似的问题。
缺点:不能知道对象识别的问题(对象的类型不知道)。
复杂的工厂模式定义是:将其成员对象的实列化推迟到子类中,子类可以重写父类接口方法以便创建的时候指定自己的对象类型。
父类只对创建过程中的一般性问题进行处理,这些处理会被子类继承,子类之间是相互独立的,具体的业务逻辑会放在子类中进行编写。
父类就变成了一个抽象类,但是父类可以执行子类中相同类似的方法,具体的业务逻辑需要放在子类中去实现;比如我现在开几个自行车店,那么每个店都有几种型号的自行车出售。我们现在来使用工厂模式来编写这些代码;
父类的构造函数如下:
// 定义自行车的构造函数
var BicycleShop = function(){
};
BicycleShop.prototype = {
constructor: BicycleShop,
/*
* 买自行车这个方法
* @param {model} 自行车型号
*/
sellBicycle: function(model){
var bicycle = this.createBicycle(mode);
// 执行A业务逻辑
bicycle.A();
// 执行B业务逻辑
bicycle.B();
return bicycle;
},
createBicycle: function(model){
throw new Error("父类是抽象类不能直接调用,需要子类重写该方法");
}
};
上面是定义一个自行车抽象类来编写工厂模式的实列,定义了createBicycle这个方法,但是如果直接实例化父类,调用父类中的这个createBicycle 方法,会抛出一个error,因为父类是一个抽象类,他不能被实列化,只能通过子类来实现这个方法,实现自己的业务逻辑,下面我们来定义子类,我们学会如何使用工厂模式重新编写这个方法,首先我们需要继承父类中的成员,然后编写子类 ;如下代码:
// 定义自行车的构造函数
var BicycleShop = function(name){
this.name = name;
this.method = function(){
return this.name;
}
};
BicycleShop.prototype = {
constructor: BicycleShop,
/*
* 买自行车这个方法
* @param {model} 自行车型号
*/
sellBicycle: function(model){
var bicycle = this.createBicycle(model);
// 执行A业务逻辑
bicycle.A();
// 执行B业务逻辑
bicycle.B();
return bicycle;
},
createBicycle: function(model){
throw new Error("父类是抽象类不能直接调用,需要子类重写该方法");
}
};
// 实现原型继承
function extend(Sub,Sup) {
//Sub表示子类,Sup表示超类
// 首先定义一个空函数
var F = function(){
};
// 设置空函数的原型为超类的原型
F.prototype = Sup.prototype;
// 实例化空函数,并把超类原型引用传递给子类
Sub.prototype = new F();
// 重置子类原型的构造器为子类自身
Sub.prototype.constructor = Sub;
// 在子类中保存超类的原型,避免子类与超类耦合
Sub.sup = Sup.prototype;
if(Sup.prototype.constructor === Object.prototype.constructor) {
// 检测超类原型的构造器是否为原型自身
Sup.prototype.constructor = Sup;
}
}
var BicycleChild = function(name){
this.name = name;
// 继承构造函数父类中的属性和方法
BicycleShop.call(this,name);
};
// 子类继承父类原型方法
extend(BicycleChild,BicycleShop);
// BicycleChild 子类重写父类的方法
BicycleChild.prototype.createBicycle = function(){
var A = function(){
console.log("执行A业务操作");
};
var B = function(){
console.log("执行B业务操作");
};
return {
A: A,
B: B
}
}
var childClass = new BicycleChild("龙恩");
console.log(childClass);
实例化子类,然后打印出该实例
console.log(childClass.name); // 龙恩
// 下面是实例化后 执行父类中的sellBicycle这个方法后会依次调用父类中的A
// 和B方法;A方法和 B方法依次在子类中去编写具体的业务逻辑。
childClass.sellBicycle(“mode”); // 打印出 执行A业务操作和执行 B业务操作
上面只是"龙恩"自行车这么一个型号的,如果需要生成其他型号的自行车的话,可以编写其他子类,工厂模式最重要的优点是:可以实现一些相同的方法,这些相同的方法我们可以放在父类中编写代码,那么需要实现具体的业务逻辑,那么可以放在子类中重写该父类的方法,去实现自己的业务逻辑;使用专业术语来讲的话有 2点:第一:弱化对象间的耦合,防止代码的重复。在一个方法中进行类的实例化,可以消除重复性的代码。第二:重复性的代码可以放在父类去编写,子类继承于父类的所有成员属性和方法,子类只专注于实现自己的业务逻辑。
理解单体模式
单体模式提供了一种将代码组织为一个逻辑单元的手段,这个逻辑单元中的代码可以通过单一变量进行访问。
单体模式的优点是:
- 可以用来划分命名空间,减少全局变量的数量。
- 使用单体模式可以使代码组织的更为一致,使代码容易阅读和维护。
- 可以被实例化,且实例化一次。
什么是单体模式?单体模式是一个用来划分命名空间并将一批属性和方法组织在一起的对象,如果它可以被实例化,那么它只能被实例化一次。
但是并非所有的对象字面量都是单体,比如说模拟数组或容纳数据的话,那么它就不是单体,但是如果是组织一批相关的属性和方法在一起的话,那么它有可能是单体模式,所以这需要看开发者编写代码的意图;
下面我们来看看定义一个对象字面量(结构类似于单体模式)的基本结构如下:
// 对象字面量
var Singleton = {
attr1: 1,
attr2: 2,
method1: function(){
return this.attr1;
},
method2: function(){
return this.attr2;
}
};
如上面只是简单的字面量结构,上面的所有成员变量都是通过Singleton来访问的,但是它并不是单体模式;因为单体模式还有一个更重要的特点,就是可以仅被实例化一次,上面的只是不能被实例化的一个类,因此不是单体模式;对象字面量是用来创建单体模式的方法之一;
使用单体模式的结构如下demo
我们明白的是单体模式如果有实例化的话,那么只实例化一次,要实现一个单体模式的话,我们无非就是使用一个变量来标识该类是否被实例化,如果未被实例化的话,那么我们可以实例化一次,否则的话,直接返回已经被实例化的对象。
如下代码是单体模式的基本结构:
// 单体模式
var Singleton = function(name){
this.name = name;
this.instance = null;
};
Singleton.prototype.getName = function(){
return this.name;
}
// 获取实例对象
function getInstance(name) {
if(!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
}
// 测试单体模式的实例
var a = getInstance("aa");
var b = getInstance("bb");
// 因为单体模式是只实例化一次,所以下面的实例是相等的
console.log(a === b); // true
由于单体模式只实例化一次,因此第一次调用,返回的是a实例对象,当我们继续调用的时候,b的实例就是a 的实例,因此下面都是打印的是aa;
console.log(a.getName());// aa
console.log(b.getName());// aa
上面的封装单体模式也可以改成如下结构写法:
// 单体模式
var Singleton = function(name){
this.name = name;
};
Singleton.prototype.getName = function(){
return this.name;
}
// 获取实例对象
var getInstance = (function() {
var instance = null;
return function(name) {
if(!instance) {
instance = new Singleton(name);
}
return instance;
}
})();
// 测试单体模式的实例
var a = getInstance("aa");
var b = getInstance("bb");
// 因为单体模式是只实例化一次,所以下面的实例是相等的
console.log(a === b); // true
console.log(a.getName());// aa
console.log(b.getName());// aa
理解使用代理实现单列模式的好处
比如我现在页面上需要创建一个div的元素,那么我们肯定需要有一个创建 div的函数,而现在我只需要这个函数只负责创建div元素,其他的它不想管,也就是想实现单一职责原则,就好比淘宝的kissy 一样,一开始的时候他们定义kissy只做一件事,并且把这件事做好,具体的单体模式中的实例化类的事情交给代理函数去处理,这样做的好处是具体的业务逻辑分开了,代理只管代理的业务逻辑,在这里代理的作用是实例化对象,并且只实例化一次; 创建div代码只管创建div,其他的不管;如下代码:
// 单体模式
var CreateDiv = function(html) {
this.html = html;
this.init();
}
CreateDiv.prototype.init = function(){
var div = document.createElement("div");
div.innerHTML = this.html;
document.body