一. 模板引擎
将数据变为视图的解决方案。
历史用到的数据转视图的方法
纯dom join 反引号 mustache
mustache
最早的模板引擎库。
(1)基本使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script src="./lib/mustache.js"></script>
<script>
var data = {
arr: [11, 22, 33, 44, 55, 66],
name: "模板引擎"
}
var templateStr = `
<ul>
{{#arr}}
<li>{{.}}</li>
{{/arr}}
</ul>
`
var domStr = Mustache.render(templateStr, data)
console.log(domStr)
var root = document.getElementById("root")
root.innerHTML = domStr
let Fragment = document.createElement("div")
var kidHtml = `
<div>{{name}}</div>
`
let kid = Mustache.render(kidHtml, data)
Fragment.innerHTML = kid
root.appendChild(Fragment)
</script>
</body>
</html>
(2)源码解析
将html模板字符串解析为二维数组,也叫tokens
(3)模拟实现
使用的webpack环境
入口html(用来测试)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Webpack App</title>
<style>
.index {
color: red;
}
</style>
</head>
<body>
<h1>Hello world!</h1>
<h2>Tip: Check your console</h2>
<div id="root"></div>
<script src="main.js"></script>
<script>
function data() {
return {
name: "小黄",
age: "11",
life: { mood: "happy" },
arr: [
{ index: "1", list: ["吃饭1", "睡觉1"] },
{ index: "2", list: ["吃饭2", "睡觉2"] },
{ index: "3", list: ["吃饭3", "睡觉3"] },
]
}
}
let templateStr = `
<div>数据的名字是:{{name}},年龄是:{{age}},今日心情:{{life.mood}}</div>
<ul>
{{#arr}}
<li class="index">{{index}}</li>
{{#list}}
<div>{{.}}</div>
{{/list}}
{{/arr}}
</ul>
`
let kidStr = MyTemplate.render(templateStr, data())
let root = document.getElementById("root")
root.innerHTML = kidStr
</script>
</body>
</html>
入口js
import TemplateToTokens from "./TemplateToTokens"
import renderTemplate from './renderTemplate'
window.MyTemplate = {
render(templateStr, data) {
let tokens = TemplateToTokens(templateStr)
let domStr = renderTemplate(tokens, data)
return domStr
}
}
模板转tokens (TemplateToTokens.js)
//负责将模板字符串转化为tokens
import scanner from "./scanner";
import nestTokens from './nestTokens'
export default function TemplateToTokens(templateStr) {
var tokens = []
var token
let scanObj = new scanner(templateStr)
while (scanObj.eos()) {
//{{之前的数据
token = scanObj.scanUntil("{{")
if (token !== "")
tokens.push(["text", token])
scanObj.scan("{{")
token = scanObj.scanUntil("}}")
if (token !== "") {
if (token.charAt(0) === "#") {
tokens.push(["#", token.substring(1)])
} else if (token.charAt(0) === "/") {
tokens.push(["/", token])
} else {
tokens.push(["name", token])
}
}
scanObj.scan("}}")
}
return nestTokens(tokens)
}
scanner用于扫描模板字符串
//负责按条件扫描模板字符串
export default class scanner {
constructor(templateStr) {
this.templateStr = templateStr
//指针
this.pos = 0
//尾巴一开始就是原文
this.tail = templateStr
}
//跳过指定内容
scan(tag) {
if (this.tail.indexOf(tag) == 0) {
this.pos += tag.length
}
this.tail = this.templateStr.substring(this.pos)
}
//走过字符串并返回
scanUntil(stopTag) {
//记录开始的pos
const pos_backup = this.pos
while (this.eos() && this.tail.indexOf(stopTag) != 0) {
this.pos++;
//从当前指针到结束的所有字符
this.tail = this.templateStr.substring(this.pos)
}
return this.templateStr.substring(pos_backup, this.pos)
}
eos() {
return this.pos < this.templateStr.length
}
}
压缩tokens,主要为了处理循环遍历对象 数组的情况 (nestTokens.js)
//折叠tokens 主要是处理循环遍历数据的token
export default function nestTokens(tokens) {
let nestedTokens = []
let sections = []
let collector = nestedTokens
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i]
switch (token[0]) {
case "#":
collector.push(token)
sections.push(token)
collector = token[2] = []
break;
case "/":
sections.pop()
collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens
break
default:
collector.push(token)
break;
}
}
return nestedTokens
}
将tokens与数据结合生成最终字符串(renderTemplate.js)
import lookup from "./lookup"
export default function renderTemplate(tokens, data) {
let resultStr = ""
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i]
if (token[0] === "text") {
resultStr = resultStr + token[1]
} else if (token[0] === "name") {
if (token[1] === ".") {
resultStr = resultStr + data
} else {
resultStr = resultStr + lookup(data,token[1])
}
} else if (token[0] === "#") {
let target = data[token[1]]
for (let j = 0; j < target.length; j++) {
resultStr = resultStr + renderTemplate(token[2], target[j])
}
}
}
return resultStr
}
lookup用于处理连续.符号,例如item.a.b
//识别连续.符号 a.b.c
export default function lookup(dataObj, keyname) {
let targetArray = keyname.split(".")
let res = dataObj
for (let item of targetArray) {
res = res[item]
}
return res
}
二. 虚拟dom和diff算法
开山鼻祖的一个库 snabbdom
h函数用来产生虚拟dom
patch函数则可以把虚拟dom挂载到真实的dom树上。
(1)demo实现
使用webpack环境
npm i snabbdom --save安装库
在demo中实现
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
//创建patch函数
const patch = init([classModule, propsModule, styleModule, eventListenersModule])
//创建虚拟节点
var myVnode = h('a', { props: { href: 'http://www.baidu.com' } }, "百度")
console.log(myVnode);
//让虚拟dom上树
const container = document.getElementById('container')
patch(container,myVnode)
(2)手写h函数
import vnode from "./vnode";
export default function (sel, data, c) {
if (arguments.length != 3) {
throw new Error("参数错误")
}
if (typeof c == 'string' || typeof c == 'number') {
return vnode(sel, data, undefined, c, undefined)
} else if (Array.isArray(c)) {
let children=[]
for (let i = 0; i < c.length; i++) {
if (!(typeof c[i] == 'object' && c[i].hasOwnProperty('sel'))) {
throw new Error("数组子项错误")
}
children.push(c[i])
}
return vnode(sel,data,children,undefined,undefined)
} else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
let children=[]
children.push(c)
return vnode(sel,data,children,undefined,undefined)
} else {
throw new Error("第三个参数类型错误")
}
}
vnode.js
很简单就是返回组合对象
export default function (sel, data, children, text, elm) {
return { sel, data, children, text, elm }
}
测试用例
import h from "./mySnabbdom/h.js"
var myVnode= h('div',{},[
h('span',{},"111"),
h('span',{},"222"),
h('span',{},"333"),
h('span',{},h('p',{},'收尾')),
])
console.log(myVnode)
结果
(3)diffing 算法
var myVnode= h('div',{},[
h('span',{key:1},"111"),
h('span',{key:2},"222"),
h('span',{key:3},"333"),
h('span',{key:4},h('p',{},'收尾')),
])
这是我们的虚拟节点。key用作标识符,为了提高diffing算法的效率,处理逆序添加全部刷新的情况。
当页面改变时,调用patch函数(vnode旧,vnode新),patch函数进行比对,完成最小化更新。
同一节点精细化比较
(4)源码分析
总结一下整个流程
① 虚拟dom挂载到真实dom树
首先通过h函数生成虚拟dom
第一次将虚拟dom挂载到容器上(真实dom树上)
使用patch函数(真实dom,虚拟dom)
patch会将虚拟dom转化为真实dom,然后挂载到真实dom上并且删除原来的容器
例如
const container = document.getElementById("container")
patch(container, myVnode)
我们将虚拟dom放在container容器中
最终生成的dom树会删掉原来的节点
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Webpack App</title>
</head>
<body>
<div id="container"></div>
</body>
</html>
最终生成
可以看到最终生成后是没有container这个div的
② diff算法
将新的虚拟dom与旧的虚拟dom进行精细化比较
提出四中命中方法 (为了对付新旧节点都有children的情况)
这是经典的比对差异的算法
这个就是按照顺序一次查询,只要命中一个就不在继续查找。
对应于我们的四个指针
例如 前两个节点一致,就命中第一种 新前与旧前
那么旧前指针+1 新前指针+1 并且不在查询其他的命中情况 开始判断第二个节点
详细的将
命中新前旧前 两个前指针下移
命中新后与旧后 两个后指针上移
命中新后与旧前 移动新前指向的节点到老节点的旧后后面(这个要解释一下,当都没命中的时就会使用循环语句获取旧节点中的新前指向节点,通过比对key和sel,然后将该节点移动到旧后指向节点之后,将原来位置的该节点设置为undefined))
命中新前与旧后 移动新前指向的节点到老节点的旧前前面(因为当都没命中的时就会使用循环语句获取旧节点中的新前指向节点,通过比对key和sel,然后将该节点移动到旧前指向节点之前,将原来位置的该节点设置为undefined)
③实现手写四种命中的核心逻辑
updateChildren.js
//处理新旧节点都有children的情况
import patchVnode from "./patchVnode"
import createElement from "./createElement"
function sameNode(a, b) {
return (a.key === b.key && a.sel === b.sel)
}
export default function (pElm, oldCh, newCh) {
//四个指针
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let newEndIdx = newCh.length - 1
//四个节点
let oldStartVnode = oldCh[0]
let newStartVnode = newCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndVnode = newCh[newEndIdx]
let keymap = null //key缓存
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
console.log("★")
//略过undefined
if (oldStartVnode == null || oldCh[oldStartIdx] == undefined) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (oldEndVnode == null || oldCh[oldEndIdx] == undefined) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (newStartVnode == null || newCh[newStartIdx] == undefined) {
newStartVnode = newCh[++newStartIdx]
} else if (newEndVnode == null || newCh[newEndIdx] == undefined) {
newEndVnode = newCh[--newEndIdx]
} else if (sameNode(oldStartVnode, newStartVnode)) {
//命中新前旧前
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameNode(newEndVnode, oldEndVnode)) {
//新后与旧后
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameNode(newEndVnode, oldStartVnode)) {
//新后与旧前
patchVnode(oldStartVnode, newEndVnode)
//移动新前指向的节点到老节点的旧后后面
pElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameNode(newStartVnode, oldEndVnode)) {
//新前与旧后
patchVnode(oldEndVnode, newStartVnode)
//移动新前指向的节点到老节点的旧前前面
pElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newStartVnode[++newStartIdx]
} else {
//四种方式都没命中
// 寻找keymap
if (!keymap) {
keymap = {}
//将key与索引存为键值对
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldCh[i].key
if (key != undefined) {
keymap[key] = i
}
}
}
console.log(keymap)
//获取新节点是否在旧节点map中出现
const idxInOld = keymap[newStartVnode.key]
if (idxInOld == undefined) {
//说明是全新的项
pElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
} else {
//移动
const eleToMove = oldCh[idxInOld]
if (eleToMove) {
patchVnode(eleToMove, newStartVnode)
//把这项设置为undefined
oldCh[idxInOld] = undefined;
// 移动
pElm.insertBefore(eleToMove.elm, oldStartVnode.elm)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
//循环结束继续处理
if (newStartIdx <= newEndIdx) {
//还有新的
for (let i = newStartIdx; i <= newEndIdx; i++) {
//insertBefrore参考节点为null时自动添加到末尾
pElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm)
}
} else if (oldStartIdx <= oldEndIdx) {
// old还有旧的
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if (oldCh[i])
pElm.removeChild(oldCh[i].elm)
}
}
}
④ 最终结构
webpack环境
(test.js仅用来测试没啥用)
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Webpack App</title>
</head>
<body>
<div id="container"></div>
</body>
</html>
h.js
import vnode from "./vnode";
export default function (sel, data, c) {
if (arguments.length != 3) {
throw new Error("参数错误")
}
if (typeof c == 'string' || typeof c == 'number') {
return vnode(sel, data, undefined, c, undefined)
} else if (Array.isArray(c)) {
let children=[]
for (let i = 0; i < c.length; i++) {
if (!(typeof c[i] == 'object' && c[i].hasOwnProperty('sel'))) {
throw new Error("数组子项错误")
}
children.push(c[i])
}
return vnode(sel,data,children,undefined,undefined)
} else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
let children=[]
children.push(c)
return vnode(sel,data,children,undefined,undefined)
} else {
throw new Error("第三个参数类型错误")
}
}
vnode.js
export default function (sel, data, children, text, elm) {
const { key } = data
if (key !== undefined) {
return { sel, data, children, text, elm, key }
}
return { sel, data, children, text, elm }
}
patch.js
import vnode from "./vnode";
import createElement from './createElement'
import patchVnode from "./patchVnode";
export default function (oldNode, newNode) {
//判断第一个节点是否为虚拟节点
if (oldNode.sel == '' || oldNode.sel == undefined) {
oldNode = vnode(oldNode.tagName.toLowerCase(), {}, [], undefined, oldNode)
oldNode.key = "realDom"
}
//同一个节点精细比较
if (sameNode()) {
console.log("相同节点");
patchVnode(oldNode,newNode)
} else {
// 不是同一个节点,暴力插入新的,删除旧的
console.log("暴力插入");
let newRealDom = createElement(newNode)
if (oldNode.elm.parentNode && newRealDom) {
oldNode.elm.parentNode.insertBefore(newRealDom, oldNode.elm)
oldNode.elm.remove()
}
}
//判断是不是同一个节点
function sameNode() {
return (oldNode.key == newNode.key && oldNode.sel == newNode.sel)
}
}
createElement.js
//创建真正的节点,将vnode创建为dom
function createElement(vnode) {
let domNode = document.createElement(vnode.sel)
if (vnode.text != '' && vnode.children == undefined) {
domNode.innerText = vnode.text
} else if (Array.isArray(vnode.children) && vnode.children.length != 0) {
for (let item of vnode.children) {
let chNode = createElement(item)
domNode.appendChild(chNode)
}
}
vnode.elm = domNode
return vnode.elm
}
export default createElement
patchVnode.js
import updateChildren from "./updateChildren"
export default function (oldNode, newNode) {
if (oldNode == newNode) {
return console.log("同一节点不处理")
}
//新节点有text
if (newNode.text != undefined) {
//新老text不同 老的就算是有子节点也会被替换掉
if (oldNode.text != newNode.text) {
oldNode.elm.innerText = newNode.text
}
//新节点无text 有children
} else {
//新老都有children
if (oldNode.children != undefined && oldNode.children.length > 0) {
//运用四种命中策略
//新前旧前
//新后旧后
//新后旧前
//新前旧后
updateChildren(oldNode.elm, oldNode.children, newNode.children)
}
}
}
updateChildren.js
//处理新旧节点都有children的情况
import patchVnode from "./patchVnode"
import createElement from "./createElement"
function sameNode(a, b) {
return (a.key === b.key && a.sel === b.sel)
}
export default function (pElm, oldCh, newCh) {
//四个指针
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let newEndIdx = newCh.length - 1
//四个节点
let oldStartVnode = oldCh[0]
let newStartVnode = newCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndVnode = newCh[newEndIdx]
let keymap = null //key缓存
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
console.log("★")
//略过undefined
if (oldStartVnode == null || oldCh[oldStartIdx] == undefined) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (oldEndVnode == null || oldCh[oldEndIdx] == undefined) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (newStartVnode == null || newCh[newStartIdx] == undefined) {
newStartVnode = newCh[++newStartIdx]
} else if (newEndVnode == null || newCh[newEndIdx] == undefined) {
newEndVnode = newCh[--newEndIdx]
} else if (sameNode(oldStartVnode, newStartVnode)) {
//命中新前旧前
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameNode(newEndVnode, oldEndVnode)) {
//新后与旧后
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameNode(newEndVnode, oldStartVnode)) {
//新后与旧前
patchVnode(oldStartVnode, newEndVnode)
//移动新前指向的节点到老节点的旧后后面
pElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameNode(newStartVnode, oldEndVnode)) {
//新前与旧后
patchVnode(oldEndVnode, newStartVnode)
//移动新前指向的节点到老节点的旧前前面
pElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newStartVnode[++newStartIdx]
} else {
//四种方式都没命中
// 寻找keymap
if (!keymap) {
keymap = {}
//将key与索引存为键值对
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldCh[i].key
if (key != undefined) {
keymap[key] = i
}
}
}
console.log(keymap)
//获取新节点是否在旧节点map中出现
const idxInOld = keymap[newStartVnode.key]
if (idxInOld == undefined) {
//说明是全新的项
pElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
} else {
//移动
const eleToMove = oldCh[idxInOld]
if (eleToMove) {
patchVnode(eleToMove, newStartVnode)
//把这项设置为undefined
oldCh[idxInOld] = undefined;
// 移动
pElm.insertBefore(eleToMove.elm, oldStartVnode.elm)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
//循环结束继续处理
if (newStartIdx <= newEndIdx) {
//还有新的
for (let i = newStartIdx; i <= newEndIdx; i++) {
//insertBefrore参考节点为null时自动添加到末尾
pElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm)
}
} else if (oldStartIdx <= oldEndIdx) {
// old还有旧的
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if (oldCh[i])
pElm.removeChild(oldCh[i].elm)
}
}
}
测试代码(index.js)
import h from "./mySnabbdom/h.js"
import patch from "./mySnabbdom/patch.js"
var myVnode = h('div', {}, [
h('div', { key: "A" }, "111"),
h('div', { key: "B" }, "222"),
h('div', { key: "C" }, "333"),
h('div', { key: "D" }, h('p', {}, '444')),
])
let btn1 = document.createElement("button")
btn1.innerText = "倒序"
document.getElementsByTagName("body")[0].appendChild(btn1)
btn1.addEventListener("click", () => {
changeDom(myVnode, newMyVnode1)
})
let btn2 = document.createElement("button")
btn2.innerText = "乱序"
document.getElementsByTagName("body")[0].appendChild(btn2)
btn2.addEventListener("click", () => {
changeDom(myVnode, newMyVnode2)
})
let btn3 = document.createElement("button")
btn3.innerText = "四种方式都命中不到"
document.getElementsByTagName("body")[0].appendChild(btn3)
btn3.addEventListener("click", () => {
changeDom(myVnode, newMyVnode3)
})
let btn4 = document.createElement("button")
btn4.innerText = "添加"
document.getElementsByTagName("body")[0].appendChild(btn4)
btn4.addEventListener("click", () => {
changeDom(myVnode, newMyVnode4)
})
let btn5 = document.createElement("button")
btn5.innerText = "删除"
document.getElementsByTagName("body")[0].appendChild(btn5)
btn5.addEventListener("click", () => {
changeDom(myVnode, newMyVnode5)
})
let btn6 = document.createElement("button")
btn6.innerText = "移动"
document.getElementsByTagName("body")[0].appendChild(btn6)
btn6.addEventListener("click", () => {
changeDom(myVnode, newMyVnode6)
})
var newMyVnode1 = h('div', {}, [
h('div', { key: "D" }, h('p', {}, '444')),
h('div', { key: "C" }, "333"),
h('div', { key: "B" }, "222"),
h('div', { key: "A" }, "111"),
])
var newMyVnode2 = h('div', {}, [
h('div', { key: "C" }, "333"),
h('div', { key: "B" }, "222"),
h('div', { key: "D" }, h('p', {}, '444')),
h('div', { key: "A" }, "111"),
])
var newMyVnode3 = h('div', {}, [
h('div', { key: "C" }, "333"),
h('div', { key: "B" }, "222"),
h('div', { key: "D" }, h('p', {}, '444')),
h('div', { key: "A" }, "111"),
h("div", { key: 222 }, "123456")
])
var newMyVnode4 = h('div', {}, [
h('div', { key: "A" }, "111"),
h('div', { key: "B" }, "222"),
h('div', { key: "C" }, "333"),
h('div', { key: "D" }, h('p', {}, '444')),
h('div', { key: 5 }, "555"),
])
var newMyVnode5 = h('div', {}, [
h('div', { key: "A" }, "111"),
h('div', { key: "B" }, "222"),
h('div', { key: "C" }, "333"),
])
var newMyVnode6 = h('div', {}, [
h('div', { key: "B" }, "222"),
])
function changeDom(oldMyVnode, newMyVnode) {
// var newMyNode = h("div", {}, "777")
patch(oldMyVnode, newMyVnode)
}
console.log(myVnode)
const container = document.getElementById("container")
patch(container, myVnode)
这样就完成了手写snabbdom的核心逻辑。
三. 响应式原理
以vue2为例这里
对对象的监听就是递归检测,源码下面会分析,对于数组是重写了对应的七个方法:
pop push shift unshift sort reverse splice
(1). 总体流程
(2). 细节流程
初始化过程,也就是读取数据过程
data就是我们要监听的数据,watcher是一个监听者,监听一个data的属性并且传入一个回调函数。
每个属性都有自己的一个observer和dep实例对象。
Observe,负责帮我们递归将data里的所有属性都设置为响应式。与对象本身的observer不同,只是个函数。设置响应式的方式就是数据代理:
Object.defineProperty(data,key,{
get(){},
set(){}
})
Dep对应谁读取了我,我就把水存储在我的信息池里,等有一天我修改了,我就全给他们通知了。(发布订阅模式)
所以对于一个被设置响应式的属性来讲,它现在是这个样子。
ob的作用其实是在递归代理那里,需要看最后的代码部分,因为vue底层的这个递归不是靠函数自己调用自己,而是使用了循环调用的方式,大概的流程就是
1.observe(data) 然后我们的observe去看这个属性有没有__ob__,有就return ob 没有就新建一个,__ob__=new Observer(data)
import Observer from "./Observer";
export default function observe(value) {
if (typeof value !== 'object') return
var ob; //存储Observer的实例
if (typeof value.__ob__ !== 'undefined') {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
2.那么此时就调用到了我们的Observer类,Observer类初始化时又会调用walk()函数,就是遍历data的第一层属性,对他们使用definReactive(下一层属性)
import { def } from "./utils"
import defineReactive from "./defineReactive"
import { arrayMethods } from './array'
import observe from "./observe"
import Dep from "./Dep"
export default class {
constructor(value) {
//每一个observer的实例都有对应的dep
this.dep=new Dep()
this.value = value
def(value, '__ob__', this, false)
//检查是否数组
if (Array.isArray(value)) {
//更换数组原型
Object.setPrototypeOf(value, arrayMethods)
//数组的特殊遍历
this.observeArray(value)
} else {
this.walk(value)
}
}
//遍历
walk(value) {
for (let key in value) {
defineReactive(value, key)
}
}
//数组特殊遍历
observeArray(arr) {
for (let i = 0; i = arr.length; i++) {
//逐项遍历
observe(arr[i])
}
}
}
3.此时就来到了definReactive(下一层属性),这个函数就是设置数据代理的地方,那么它又会调用observe(下一层属性)。
import observe from './observe'
import Dep from './Dep'
export default function defineReactive(data, key, val) {
//相当于每个属性有自己的一个dep
let dep = new Dep()
if (arguments.length == 2) {
val = data[key]
}
//循环调用
var childOb = observe(val)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
//处于依赖收集阶段
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
}
return val
},
set(newValue) {
if (newValue === val) {
return
}
val = newValue
//检测新值
childOb = observe(newValue)
//通知改变
dep.notify()
}
})
}
4.那么就回到了1,但是这次判断的是data的下一层属性。
更改数据后更新的过程
数据修改时,会触发set方法,set方法中就会触发dep的notice方法,该方法的功能就是遍历通知所有的订阅该数据属性的watcher,那么watcher就会触发更新。
(3)手写代码实现
webpack环境
下面是完整代码(按照上面的目录顺序排列)
import { def } from './utils'
const arrayPrototype = Array.prototype
export const arrayMethods = Object.create(arrayPrototype)
//要改写的七个方法
const methodsNeedChange = [
'push',
'pop',
'shift',
'unshift',
'splice',
'reverse',
'sort'
]
methodsNeedChange.forEach(name => {
//原来的方法
const original = arrayPrototype[name]
//定义新方法
def(arrayMethods, name, function () {
//获取数组的__ob__
const ob = this.__ob__
//对应新添加的元素同样检测
let inserted = []
switch (name) {
case 'push':
case 'unshift':
inserted = arguments
break
case 'splice':
inserted = [...arguments].slice(2)
}
if (inserted) {
ob.observeArray(inserted)
}
//恢复原来的功能
let res = original.apply(this, arguments)
//提醒更新
ob.dep.notify()
return res
})
}, false)
import observe from './observe'
import Dep from './Dep'
export default function defineReactive(data, key, val) {
//相当于每个属性有自己的一个dep
let dep = new Dep()
if (arguments.length == 2) {
val = data[key]
}
//循环调用
var childOb = observe(val)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
//处于依赖收集阶段
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
}
return val
},
set(newValue) {
if (newValue === val) {
return
}
val = newValue
//检测新值
childOb = observe(newValue)
//通知改变
dep.notify()
}
})
}
var uid = 0
export default class Dep {
constructor() {
this.id = uid++
//存放watcher实例
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
depend() {
//指定的全局位置
if (Dep.target) {
this.addSub(Dep.target)
}
}
notify() {
const subs = this.subs.slice();
for (let item of subs) {
item.update()
}
}
}
import observe from './observe'
import Watcher from './Watcher'
var data = {
a: 1,
b: 2,
c: {
test: "test"
}
}
observe(data)
new Watcher(data, 'c.test', (val) => {
console.log('新值为', val)
})
data.c.test = "111"
import Observer from "./Observer";
export default function observe(value) {
if (typeof value !== 'object') return
var ob; //存储Observer的实例
if (typeof value.__ob__ !== 'undefined') {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
import { def } from "./utils"
import defineReactive from "./defineReactive"
import { arrayMethods } from './array'
import observe from "./observe"
import Dep from "./Dep"
export default class {
constructor(value) {
//每一个observer的实例都有对应的dep
this.dep=new Dep()
this.value = value
def(value, '__ob__', this, false)
//检查是否数组
if (Array.isArray(value)) {
//更换数组原型
Object.setPrototypeOf(value, arrayMethods)
//数组的特殊遍历
this.observeArray(value)
} else {
this.walk(value)
}
}
//遍历
walk(value) {
for (let key in value) {
defineReactive(value, key)
}
}
//数组特殊遍历
observeArray(arr) {
for (let i = 0; i = arr.length; i++) {
//逐项遍历
observe(arr[i])
}
}
}
export function def(obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true
})
}
import Dep from "./Dep"
var uid = 0
export default class Watcher {
constructor(target, expression, callback) {
this.id = uid++
this.target = target
this.getter = pasrsePath(expression)
this.callback = callback
//获取初始值(属性初始值)
this.value = this.get()
}
update() {
this.run()
}
get() {
//把全局变量变为依赖本身
Dep.target = this
const obj = this.target
var value
try {
value = this.getter(obj)
} finally {
Dep.target = null
}
return value
}
run() {
this.getAndInvoke(this.callback)
}
getAndInvoke(callback) {
const value = this.get()
if (value !== this.value || typeof value == 'object') {
const oldValue = this.value
this.value = value
callback.call(this.target, value, oldValue)
}
}
}
function pasrsePath(str) {
var segments = str.split(".")
return (obj) => {
for (let item of segments) {
if (!obj) return
obj = obj[item]
}
return obj
}
}
四. AST抽象语法树
(1)抽象语法树的定义和作用
将模板语法解析为js对象。
再vue渲染中的作用