Vue 源码 – mustache 模版引擎
1. 什么是模版引擎
数据 --> 视图
纯 DOM 法 数组 join 法 ES6 反引号 模版引擎 模版引擎是将数据变为视图最优雅的解决方案
[
{ "name" : "张三" , "age" : 18 } ,
{ "name" : "李四" , "age" : 20 } ,
{ "name" : "王五" , "age" : 22 }
]
< ul>
< li>
< div class = "name" > 姓名:张三< / div>
< div class = "age" > 年龄:18 < / div>
< / li>
< li>
< div class = "name" > 姓名:李四< / div>
< div class = "age" > 年龄:20 < / div>
< / li>
< li>
< div class = "name" > 姓名:王五< / div>
< div class = "age" > 年龄:22 < / div>
< / li>
< / ul>
2. mustache 基本使用
2.1 mustache 库简介
2.2 mustache 库基本使用
2.2.1 引入 mustache 库
2.2.2 循环对象数组
Mustache.render(templateStr, data)
< div id = " container" > </ div>
< script src = " https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js" > </ script>
< script>
var templateStr = `
<ul>
// 模版语法
{{#arr}} // 从#开始循环arr这个数组,name、sex、age都是arr数组里的值
<li>
<div class="name">姓名:{{name}}</div>
<div class="age">年龄:{{age}}</div>
</li>
{{/arr}}
</ul>
`
var data = {
arr: [
{ "name" : "张三" , "age" : 18 } ,
{ "name" : "李四" , "age" : 20 } ,
{ "name" : "王五" , "age" : 22 }
]
}
var domStr = Mustache. render ( templateStr, data)
var container = document. getElementById ( 'container' ) ;
container. innerHTML = domStr;
</ script>
2.2.3 不循环
Mustache.render(templateStr, data)
< div id = " container" > </ div>
< script src = " https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js" > </ script>
< script>
var templateStr = `
<h1>我是{{name}},今年{{age}}岁</h1>
`
var data = {
name: '张三' ,
age: 18
}
var domStr = Mustache. render ( templateStr, data)
var container = document. getElementById ( 'container' ) ;
container. innerHTML = domStr;
</ script>
2.2.4 循环简单数组
Mustache.render(templateStr, data)
< div id = " container" > </ div>
< script src = " https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js" > </ script>
< script>
var templateStr = `
<ul>
{{#arr}}
<li>{{.}}</li>
{{/arr}}
</ul>
`
var data = {
arr: [ 'A' , 'B' , 'C' ]
}
var domStr = Mustache. render ( templateStr, data)
var container = document. getElementById ( 'container' ) ;
container. innerHTML = domStr;
</ script>
2.2.5 数组嵌套
Mustache.render(templateStr, data)
< div id = " container" > </ div>
< script src = " https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js" > </ script>
< script>
var templateStr = `
<ul>
{{#arr}}
<li>
{{name}}的爱好是:
<ol>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ol>
</li>
{{/arr}}
</ul>
`
var data = {
arr: [
{ 'name' : '小明' , 'age' : 12 , 'hobbies' : [ '游泳' , '羽毛球' ] } ,
{ 'name' : '小红' , 'age' : 11 , 'hobbies' : [ '编程' , '写作文' , '看报纸' ] } ,
{ 'name' : '小强' , 'age' : 13 , 'hobbies' : [ '打台球' ] } ,
]
}
var domStr = Mustache. render ( templateStr, data)
var container = document. getElementById ( 'container' ) ;
container. innerHTML = domStr;
</ script>
2.2.6 布尔值
Mustache.render(templateStr, data)
< div id = " container" > </ div>
< script src = " https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js" > </ script>
< script>
var templateStr = `
{{#m}}
<h1>你好</h1>
{{/m}}
`
var data = {
m: false
}
var domStr = Mustache. render ( templateStr, data)
var container = document. getElementById ( 'container' ) ;
container. innerHTML = domStr;
</ script>
3. mustache 底层核心原理
3.1 正则表达式思路
在简单情况下,可以用正则表达式实现 但当情况复杂时,正则表达式的思路就不行了
var templateStr = `
<h1>我是{{name}},今年{{age}}岁</h1>
`
var data = {
name: '张三' ,
age: 18
}
function render ( templateStr, data ) {
return templateStr. replace ( / \{\{(\w+)\}\} / g , function ( findStr, $1 ) {
return data[ $1 ] ;
} ) ;
}
var result = render ( templateStr, data) ;
console. log ( result) ;
3.2 mustache 库的原理
Mustache 库底层重点做了两件事
将模版字符串编译为 tokens 形式
将 tokens 结合数据,解析为 DOM 字符串
3.2.1 什么是 tokens
// 模版字符串
< h1> 我是{{name}},今年{{age}}岁</ h1>
// tokens
[
["text", "< h1> 我是"],
["name", "name"],
["text", ",今年"],
["name", "age"],
["text", "岁</ h1> "]
]
3.2.2 循环情况下的 tokens
当模版字符串中有循环
存在时,它将被编译为嵌套更深的 tokens
// 模版字符串
< div>
< ul>
{{#arr}}
< li> {{.}}</ li>
{{/arr}}
</ ul>
</ div>
// tokens
[
["text", "< div> < ul> "],
["#", "arr", [
["text", "< li> "],
["name", "."],
["text", "</ li> "]
]],
["text", "< ul> </ div> "]
]
3.2.3 双重循环情况下的 tokens
// 模版字符串
< div>
< ol>
{{#students}}
< li>
学生
< ol>
{{#hobbies}}
< li> {{.}}</ li>
{{/hobbies}}
</ ol>
</ li>
{{/students}}
</ ol>
</ div>
// tokens
[
["text", "< div> < ol> "],
["#", "students", [
["text", "< li> 学生"],
["name", "name"],
["text", "的爱好是< ol> "],
["#", "hobbies", [
["text", "< li> "],
["name", "."],
["text", "</ li> "],
]],
["text", "</ ol> </ li> "],
]],
["text", "</ ol> </ div> "]
]
4. 手写 mustache 库
4.1 构建项目
4.1.1 使用 webpack 和 webpack-dev-server 构建
Mustache 官方库使用 rollup
进行模块化打包 但是我这里使用 webpack ( webpack-dev-server )
进行模块化打包
webpack 能更方便的在浏览器中实时调用程序 ( 相比于 nodejs 控制台,浏览器控制台更好用 )
生成库是 UMD
的,这意味着它可以同时在 nodejs 环境中使用,也可以在浏览器环境中使用
4.1.2 webpack.config.js 文件
const path = require ( 'path' ) ;
module. exports = {
mode: 'development' ,
entry: './index.js' ,
output: {
filename: 'bundle.js'
} ,
devServer: {
contentBase: path. join ( dirname, "www" ) ,
compress: false ,
port: 8080 ,
publicPath: "/xuni/"
}
} ;
4.2 整体结构
< body>
< div id = " container" > </ div>
< script src = " /xuni/bundle.js" > </ script>
< script>
var templateStr = `
<div>
<ul>
{{#students}}
<li class="myli">
学生{{name}}的爱好是
<ol>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ol>
</li>
{{/students}}
</ul>
</div>
` ;
var data = {
students: [
{ 'name' : '小明' , 'hobbies' : [ '编程' , '游泳' ] } ,
{ 'name' : '小红' , 'hobbies' : [ '看书' , '弹琴' , '画画' ] } ,
{ 'name' : '小强' , 'hobbies' : [ '锻炼' ] }
]
} ;
var domStr = test_TemplateEngine. render ( templateStr, data) ;
console. log ( domStr) ;
var container = document. getElementById ( 'container' ) ;
container. innerHTML = domStr;
</ script>
</ body>
window. test_TemplateEngine = {
render ( templateStr, data ) {
var tokens = parseTemplateToTokens ( templateStr) ;
var domStr = renderTemplate ( tokens, data) ;
return domStr;
}
} ;
4.3 具体方法
4.3.1 如何将模版字符串变为 tokens
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) ;
}
}
scanUtil ( stopTag ) {
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
export default function nestTokens ( tokens ) {
var nestedTokens = [ ] ;
var sections = [ ] ;
var 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) ;
}
}
return nestedTokens;
} ;
import Scanner from './Scanner.js' ;
import nestTokens from './nestTokens.js' ;
export default function parseTemplateToTokens ( templateStr ) {
var tokens = [ ] ;
var scanner = new Scanner ( templateStr) ;
var words;
while ( ! scanner. eos ( ) ) {
words = scanner. scanUtil ( '{{' ) ;
if ( words != '' ) {
let isInJJH = false ;
var _words = '' ;
for ( let i = 0 ; i < words. length; i++ ) {
if ( words[ i] == '<' ) {
isInJJH = true ;
} else if ( words[ i] == '>' ) {
isInJJH = false ;
}
if ( ! / \s / . test ( words[ i] ) ) {
_words += words[ i] ;
} else {
if ( isInJJH) {
_words += ' ' ;
}
}
}
tokens. push ( [ 'text' , _words] ) ;
}
scanner. scan ( '{{' ) ;
words = scanner. scanUtil ( '}}' ) ;
if ( words != '' ) {
if ( words[ 0 ] == '#' ) {
tokens. push ( [ '#' , words. substring ( 1 ) ] ) ;
} else if ( words[ 0 ] == '/' ) {
tokens. push ( [ '/' , words. substring ( 1 ) ] ) ;
} else {
tokens. push ( [ 'name' , words] ) ;
}
}
scanner. scan ( '}}' ) ;
}
return nestTokens ( tokens) ;
}
4.3.2 如何将 tokens 数组变为 DOM 字符串
#
标记的 tokens,需要递归处理
下标为2的小数组注意
lookup.js
export default function lookup ( dataObj, keyName ) {
if ( keyName. indexOf ( '.' ) != - 1 && keyName != '.' ) {
var keys = keyName. split ( '.' ) ;
var temp = dataObj;
for ( let i = 0 ; i < keys. length; i++ ) {
temp = temp[ keys[ i] ] ;
}
return temp;
}
return dataObj[ keyName] ;
} ;
import lookup from './lookup.js' ;
import renderTemplate from './renderTemplate.js' ;
export default function parseArray ( token, data ) {
var v = lookup ( data, token[ 1 ] ) ;
var resultStr = '' ;
for ( let i = 0 ; i < v. length; i++ ) {
resultStr += renderTemplate ( token[ 2 ] , {
... v[ i] ,
'.' : v[ i]
} ) ;
}
return resultStr;
} ;
import lookup from './lookup.js' ;
import parseArray from './parseArray.js' ;
export default function renderTemplate ( tokens, data ) {
var resultStr = '' ;
for ( let i = 0 ; i < tokens. length; i++ ) {
let token = tokens[ i] ;
if ( token[ 0 ] == 'text' ) {
resultStr += token[ 1 ] ;
} else if ( token[ 0 ] == 'name' ) {
resultStr += lookup ( data, token[ 1 ] ) ;
} else if ( token[ 0 ] == '#' ) {
resultStr += parseArray ( token, data) ;
}
}
return resultStr;
}
Vue 源码 – 虚拟 DOM 和 diff 算法
1. 虚拟 DOM 和 diff 算法简单介绍
1.1 虚拟 DOM
< div class = " box" >
< h3> 我是一个标题</ h3>
< ul>
< li> 牛奶</ li>
< li> 咖啡</ li>
< li> 可乐</ li>
</ ul>
</ div>
虚拟 DOM
用 JavaScript 对象描述 DOM 的层次结构,DOM 中的一切属性都在虚拟 DOM 中有对应的属性
{
"sel" : "div" ,
"data" : {
"class" : { "box" : true }
} ,
"children" : [
{
"sel" : "h3" ,
"data" : { } ,
"text" : "我是一个标题"
} ,
{
"sel" : "ul" ,
"data" : { } ,
"children" : [
{ "sel" : "li" , "data" : { } , "text" : "牛奶" } ,
{ "sel" : "li" , "data" : { } , "text" : "咖啡" } ,
{ "sel" : "li" , "data" : { } , "text" : "可乐" }
]
}
]
}
1.2 diff 算法
diff 算法可以进行精细化对比
,实现最小量更新
新虚拟 DOM 和老虚拟 DOM 进行 diff (精细化比较),算出应该如何最小量更新,最后反映到真正的DOM 上
< div class = " box" >
< h3> 我是一个标题</ h3>
< ul>
< li> 牛奶</ li>
< li> 咖啡</ li>
< li> 可乐</ li>
</ ul>
</ div>
变为
< div class = " box" >
< h3> 我是一个标题</ h3>
< span> 我是一个新的span</ span>
< ul>
< li> 牛奶</ li>
< li> 咖啡</ li>
< li> 可乐</ li>
< li> 雪碧</ li>
</ ul>
</ div>
{
"sel" : "div" ,
"data" : {
"class" : { "box" : true }
} ,
"children" : [
{
"sel" : "h3" ,
"text" : "我是一个标题"
} ,
{
"sel" : "ul" ,
"data" : { } ,
"children" : [
{ "sel" : "li" , "text" : "牛奶" } ,
{ "sel" : "li" , "text" : "咖啡" } ,
{ "sel" : "li" , "text" : "可乐" }
]
}
]
}
{
"sel" : "div" ,
"data" : {
"class" : { "box" : true }
} ,
"children" : [
{
"sel" : "h3" ,
"text" : "我是一个标题"
} ,
{
"sel" : "span" ,
"text" : "我是一个新的span"
} ,
{
"sel" : "ul" ,
"data" : { } ,
"children" : [
{ "sel" : "li" , "text" : "牛奶" } ,
{ "sel" : "li" , "text" : "咖啡" } ,
{ "sel" : "li" , "text" : "可乐" } ,
{ "sel" : "li" , "text" : "雪碧" }
]
}
]
}
2. snabbdom 简介和测试环境搭建
2.1 安装 snabbdom
在 git上 的 snabbdom 源码是用 TypeScript
写的,git 上并不提供编译好的 JavaScript 版本
如果要直接使用 build 出来的 JavaScript 版的 snabbdom 库
,可以从 npm 上下载
2.2 snabbdom 测试环境搭建
const path = require ( 'path' ) ;
module. exports = {
entry: './src/index.js' ,
output: {
publicPath: 'xuni' ,
filename: 'bundle.js'
} ,
devServer: {
port: 8080 ,
contentBase: 'www'
}
} ;
3. snabbdom 的 h 函数是如何工作
3.1 h 函数用来产生虚拟节点
h 函数用来产生虚拟节点(vnode)
比如这样调用 h 函数
h('a', {props: {href: 'http://www.baidu.com'}}, '张三'
将得到这样的虚拟节点
{"sel": "a", "data": {props: {href: 'http://www.baidu.com'}}, "text": "张三"}
它表示的真正的 DOM 节点
<a href="http://www.baidu.com">张三</a>"
import { init } from 'snabbdom/init' ;
import { classModule } from 'snabbdom/modules/class' ;
import { propsModule } from 'snabbdom/modules/props' ;
import { styleModule } from 'snabbdom/modules/style' ;
import { eventListenersModule } from 'snabbdom/modules/eventlisteners' ;
import { h } from 'snabbdom/h' ;
const patch = init ( [ classModule, propsModule, styleModule, eventListenersModule] ) ;
const myVnode1 = h ( 'a' , {
props: {
href: 'http://www.baidu.com' ,
target: '_blank'
}
} , '百度' ) ;
const container = document. getElementById ( 'container' ) ;
patch ( container, myVnode1) ;
3.2 虚拟节点的属性
{
children: undefined
data: { }
elm: undefined
key: undefined
sel: "div"
text: "我是一个盒子"
}
3.3 h 函数的嵌套使用
const myVnode3 = h ( 'ul' , [
h ( 'li' , { } , '苹果' ) ,
h ( 'li' , '西瓜' ) ,
h ( 'li' , [
h ( 'div' , [
h ( 'p' , '哈哈' ) ,
h ( 'p' , '嘻嘻' )
] )
] ) ,
h ( 'li' , h ( 'p' , '火龙果' ) )
] ) ;
3.4 手写 h 函数
3.4.1 相关源码
import { vnode, VNode, VNodeData } from './vnode'
import * as is from './is'
export type VNodes = VNode[ ]
export type VNodeChildElement = VNode | string | number | undefined | null
export type ArrayOrElement< T > = T | T [ ]
export type VNodeChildren = ArrayOrElement< VNodeChildElement>
function addNS ( data: any, children: VNodes | undefined , sel: string | undefined ) : void {
data. ns = 'http://www.w3.org/2000/svg'
if ( sel !== 'foreignObject' && children !== undefined ) {
for ( let i = 0 ; i < children. length; ++ i) {
const childData = children[ i] . data
if ( childData !== undefined ) {
addNS ( childData, ( children[ i] as VNode) . children as VNodes, children[ i] . sel)
}
}
}
}
export function h ( sel: string ) : VNode
export function h ( sel: string, data: VNodeData | null ) : VNode
export function h ( sel: string, children: VNodeChildren ) : VNode
export function h ( sel: string, data: VNodeData | null , children: VNodeChildren ) : VNode
export function h ( sel: any, b? : any, c? : any ) : VNode {
var data: VNodeData = { }
var children: any
var text: any
var i: number
if ( c !== undefined ) {
if ( b !== null ) {
data = b
}
if ( is. array ( c) ) {
children = c
} else if ( is. primitive ( c) ) {
text = c
} else if ( c && c. sel) {
children = [ c]
}
} else if ( b !== undefined && b !== null ) {
if ( is. array ( b) ) {
children = b
} else if ( is. primitive ( b) ) {
text = b
} else if ( b && b. sel) {
children = [ b]
} else { data = b }
}
if ( children !== undefined ) {
for ( i = 0 ; i < children. length; ++ i) {
if ( is. primitive ( children[ i] ) ) children[ i] = vnode ( undefined , undefined , undefined , children[ i] , undefined )
}
}
if (
sel[ 0 ] === 's' && sel[ 1 ] === 'v' && sel[ 2 ] === 'g' &&
( sel. length === 3 || sel[ 3 ] === '.' || sel[ 3 ] === '#' )
) {
addNS ( data, children, sel)
}
return vnode ( sel, data, children, text, undefined )
} ;
import { Hooks } from './hooks'
import { AttachData } from './helpers/attachto'
import { VNodeStyle } from './modules/style'
import { On } from './modules/eventlisteners'
import { Attrs } from './modules/attributes'
import { Classes } from './modules/class'
import { Props } from './modules/props'
import { Dataset } from './modules/dataset'
import { Hero } from './modules/hero'
export type Key = string | number
export interface VNode {
sel: string | undefined
data: VNodeData | undefined
children: Array< VNode | string> | undefined
elm: Node | undefined
text: string | undefined
key: Key | undefined
}
export interface VNodeData {
props? : Props
attrs? : Attrs
class ? : Classes
style? : VNodeStyle
dataset? : Dataset
on? : On
hero? : Hero
attachData? : AttachData
hook? : Hooks
key? : Key
ns? : string
fn? : ( ) => VNode
args? : any[ ]
[ key: string] : any
}
export function vnode ( sel: string | undefined ,
data: any | undefined ,
children: Array< VNode | string> | undefined ,
text: string | undefined ,
elm: Element | Text | undefined ) : VNode {
const key = data === undefined ? undefined : data. key
return { sel, data, children, text, elm, key }
}
3.4.2 手写源码
export default function ( sel, data, children, text, elm ) {
const key = data. key;
return {
sel, data, children, text, elm, key
} ;
}
import vnode from './vnode.js' ;
export default function ( sel, data, c ) {
if ( arguments. length != 3 )
throw new Error ( '对不起,h函数必须传入3个参数' ) ;
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 ( '传入的数组参数中有项不是h函数' ) ;
children. push ( c[ i] ) ;
}
return vnode ( sel, data, children, undefined , undefined ) ;
} else if ( typeof c == 'object' && c. hasOwnProperty ( 'sel' ) ) {
let children = [ c] ;
return vnode ( sel, data, children, undefined , undefined ) ;
} else {
throw new Error ( '传入的第三个参数类型不对' ) ;
}
} ;
4. diff 算法原理
4.1 diff 体验
最小量更新
key 很重要
key 是这个节点的唯一标识,告诉 diff 算法,在更改前后它们是同一个 DOM 节点
import { init } from 'snabbdom/init' ;
import { classModule } from 'snabbdom/modules/class' ;
import { propsModule } from 'snabbdom/modules/props' ;
import { styleModule } from 'snabbdom/modules/style' ;
import { eventListenersModule } from 'snabbdom/modules/eventlisteners' ;
import { h } from 'snabbdom/h' ;
const container = document. getElementById ( 'container' ) ;
const btn = document. getElementById ( 'btn' ) ;
const patch = init ( [ classModule, propsModule, styleModule, eventListenersModule] ) ;
const vnode1 = h ( 'ul' , { } , [
h ( 'li' , { key: 'A' } , 'A' ) ,
h ( 'li' , { key: 'B' } , 'B' ) ,
h ( 'li' , { key: 'C' } , 'C' ) ,
h ( 'li' , { key: 'D' } , 'D' )
] ) ;
patch ( container, vnode1) ;
const vnode2 = h ( 'ul' , { } , [
h ( 'li' , { key: 'D' } , 'D' ) ,
h ( 'li' , { key: 'A' } , 'A' ) ,
h ( 'li' , { key: 'C' } , 'C' ) ,
h ( 'li' , { key: 'B' } , 'B' )
] ) ;
btn. onclick = function ( ) {
patch ( vnode1, vnode2) ;
} ;
只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的
延伸问题:如何定义是同一个虚拟节点?
答:选择器相同且 key 相同
const vnode1 = h ( 'ul' , { } , [
h ( 'li' , { key: 'A' } , 'A' ) ,
h ( 'li' , { key: 'B' } , 'B' ) ,
h ( 'li' , { key: 'C' } , 'C' ) ,
h ( 'li' , { key: 'D' } , 'D' )
] ) ;
patch ( container, vnode1) ;
const vnode2 = h ( 'ol' , { } , [
h ( 'li' , { key: 'D' } , 'D' ) ,
h ( 'li' , { key: 'A' } , 'A' ) ,
h ( 'li' , { key: 'C' } , 'C' ) ,
h ( 'li' , { key: 'B' } , 'B' )
] ) ;
btn. onclick = function ( ) {
patch ( vnode1, vnode2) ;
} ;
只进行同层比较,不会进行跨层比较
即使是同一片虚拟节点,但是跨层了,不会进行 diff 算法,而是暴力删除旧的、然后插入新的
const vnode1 = h ( 'div' , { } , [
h ( 'p' , { key: 'A' } , 'A' ) ,
h ( 'p' , { key: 'B' } , 'B' ) ,
h ( 'p' , { key: 'C' } , 'C' ) ,
h ( 'p' , { key: 'D' } , 'D' )
] ) ;
patch ( container, vnode1) ;
const vnode2 = h ( 'div' , { } , h ( 'section' , { } , [
h ( 'p' , { key: 'D' } , 'D' ) ,
h ( 'p' , { key: 'A' } , 'A' ) ,
h ( 'p' , { key: 'C' } , 'C' ) ,
h ( 'p' , { key: 'B' } , 'B' )
] ) ) ;
btn. onclick = function ( ) {
patch ( vnode1, vnode2) ;
} ;
4.2 diff 处理新旧节点不是同一个节点时
4.2.1 相关源码
function emptyNodeAt ( elm: Element ) {
const id = elm. id ? '#' + elm. id : ''
const c = elm. className ? '.' + elm. className. split ( ' ' ) . join ( '.' ) : ''
return vnode ( api. tagName ( elm) . toLowerCase ( ) + id + c, { } , [ ] , undefined , elm)
}
return function patch ( oldVnode: VNode | Element, vnode: VNode ) : VNode {
let i: number, elm: Node, parent: Node
const insertedVnodeQueue: VNodeQueue = [ ]
for ( i = 0 ; i < cbs. pre. length; ++ i) cbs. pre[ i] ( )
if ( ! isVnode ( oldVnode) ) {
oldVnode = emptyNodeAt ( oldVnode)
}
if ( sameVnode ( oldVnode, vnode) ) {
patchVnode ( oldVnode, vnode, insertedVnodeQueue)
} else {
elm = oldVnode. elm!
parent = api. parentNode ( elm) as Node
createElm ( vnode, insertedVnodeQueue)
if ( parent !== null ) {
api. insertBefore ( parent, vnode. elm! , api. nextSibling ( elm) )
removeVnodes ( parent, [ oldVnode] , 0 , 0 )
}
}
for ( i = 0 ; i < insertedVnodeQueue. length; ++ i) {
insertedVnodeQueue[ i] . data! . hook! . insert! ( insertedVnodeQueue[ i] )
}
for ( i = 0 ; i < cbs. post. length; ++ i) cbs. post[ i] ( )
return vnode
}
4.2.2 流程图
4.2.3 如何定义同一个节点
function sameVnode ( vnode1: VNode, vnode2: VNode ) : boolean {
return vnode1. key === vnode2. key && vnode1. sel === vnode2. sel
}
4.2.4 递归创建子节点
function createElm ( vnode: VNode, insertedVnodeQueue: VNodeQueue ) : Node {
let i: any
let data = vnode. data
if ( data !== undefined ) {
const init = data. hook?. init
if ( isDef ( init) ) {
init ( vnode)
data = vnode. data
}
}
const children = vnode. children
const sel = vnode. sel
if ( sel === '!' ) {
if ( isUndef ( vnode. text) ) {
vnode. text = ''
}
vnode. elm = api. createComment ( vnode. text! )
} else if ( sel !== undefined ) {
const hashIdx = sel. indexOf ( '#' )
const dotIdx = sel. indexOf ( '.' , hashIdx)
const hash = hashIdx > 0 ? hashIdx : sel. length
const dot = dotIdx > 0 ? dotIdx : sel. length
const tag = hashIdx !== - 1 || dotIdx !== - 1 ? sel. slice ( 0 , Math. min ( hash, dot) ) : sel
const elm = vnode. elm = isDef ( data) && isDef ( i = data. ns)
? api. createElementNS ( i, tag)
: api. createElement ( tag)
if ( hash < dot) elm. setAttribute ( 'id' , sel. slice ( hash + 1 , dot) )
if ( dotIdx > 0 ) elm. setAttribute ( 'class' , sel. slice ( dot + 1 ) . replace ( / \. / g , ' ' ) )
for ( i = 0 ; i < cbs. create. length; ++ i) cbs. create[ i] ( emptyNode, vnode)
if ( is. array ( children) ) {
for ( i = 0 ; i < children. length; ++ i) {
const ch = children[ i]
if ( ch != null ) {
api. appendChild ( elm, createElm ( ch as VNode, insertedVnodeQueue) )
}
}
} else if ( is. primitive ( vnode. text) ) {
api. appendChild ( elm, api. createTextNode ( vnode. text) )
}
const hook = vnode. data! . hook
if ( isDef ( hook) ) {
hook. create?. ( emptyNode, vnode)
if ( hook. insert) {
insertedVnodeQueue. push ( vnode)
}
}
} else {
vnode. elm = api. createTextNode ( vnode. text! )
}
return vnode. elm
}
5. 手写 diff 算法
5.1 手写第一次渲染 DOM 树
import h from './mysnabbdom/h.js' ;
import patch from './mysnabbdom/patch.js' ;
const myVnode1 = h ( 'ul' , { } , [
h ( 'li' , { } , 'A' ) ,
h ( 'li' , { } , 'B' ) ,
h ( 'li' , { } , 'C' ) ,
h ( 'li' , { } , 'D' )
] ) ;
const container = document. getElementById ( 'container' ) ;
patch ( container, myVnode1) ;
import vnode from './vnode.js' ;
import createElement from './createElement.js' ;
export default function patch ( oldVnode, newVnode ) {
if ( oldVnode. sel = '' || oldVnode. sel = undefined ) {
oldVnode = vnode ( oldVnode. tagName. toLowerCase ( ) , { } , [ ] , undefined , oldVnode) ;
}
if ( oldVnode. key == newVnode. key && oldVnode. sel == newVnode == sel) {
console. log ( '是同一个节点' )
} else {
console. log ( '不是同一个节点,暴力插入新的,删除旧的' )
createElement ( newVnode, oldVnode. elm)
}
export default function ( vnode, pivot ) {
console. log ( '目的是把虚拟节点' , vnode, '插入到标杆' , pivot, '前' ) ; \
let domNode = document. createElement ( vnode. sel) ;
if ( vnode. text != '' && ( vnode. children == undefined || vnode. children. length == 0 ) ) {
domNode. innerText = vnode. text;
pivot. parentNode. insertBefore ( domNode, pivot)
} else if ( Array. isArray ( vnode. children) && vnode. children. length > 0 ) {
}
}
5.2 手写递归创建子节点
export default function createElement ( vnode ) {
let domNode = document. createElement ( vnode. sel) ;
if ( vnode. text != '' && ( vnode. children == undefined || vnode. children. length == 0 ) ) {
domNode. innerText = vnode. text;
vnode. elm = domNode;
} else if ( Array. isArray ( vnode. children) && vnode. children. length > 0 ) {
for ( let i = 0 ; i < vnode. children. length; i++ ) {
let ch = vnode. children[ i] ;
let chDOM = createElement ( ch) ;
domNode. appendChild ( chDOM) ;
}
}
vnode. elm = domNode;
return vnode. elm;
} ;
import vnode from './vnode.js' ;
import createElement from './createElement.js' ;
import patchVnode from './patchVnode.js'
export default function patch ( oldVnode, newVnode ) {
if ( oldVnode. sel == '' || oldVnode. sel == undefined ) {
oldVnode = vnode ( oldVnode. tagName. toLowerCase ( ) , { } , [ ] , undefined , oldVnode) ;
}
if ( oldVnode. key == newVnode. key && oldVnode. sel == newVnode. sel) {
console. log ( '是同一个节点' ) ;
patchVnode ( oldVnode, newVnode) ;
} else {
console. log ( '不是同一个节点,暴力插入新的,删除旧的' ) ;
let newVnodeElm = createElement ( newVnode) ;
if ( oldVnode. elm. parentNode && newVnodeElm) {
oldVnode. elm. parentNode. insertBefore ( newVnodeElm, oldVnode. elm) ;
}
oldVnode. elm. parentNode. removeChild ( oldVnode. elm) ;
}
} ;
import h from './mysnabbdom/h.js' ;
import patch from './mysnabbdom/patch.js' ;
const myVnode1 = h ( 'ul' , { } , [
h ( 'li' , { key: 'A' } , 'A' ) ,
h ( 'li' , { key: 'B' } , 'B' ) ,
h ( 'li' , { key: 'C' } , 'C' ) ,
h ( 'li' , { key: 'D' } , 'D' ) ,
h ( 'li' , { key: 'E' } , 'E' )
] ) ;
const container = document. getElementById ( 'container' ) ;
const btn = document. getElementById ( 'btn' ) ;
patch ( container, myVnode1) ;
const myVnode2 = h ( 'ul' , { } , [
h ( 'li' , { key: 'Q' } , 'Q' ) ,
h ( 'li' , { key: 'T' } , 'T' ) ,
h ( 'li' , { key: 'A' } , 'A' ) ,
h ( 'li' , { key: 'B' } , 'B' ) ,
h ( 'li' , { key: 'Z' } , 'Z' ) ,
h ( 'li' , { key: 'C' } , 'C' ) ,
h ( 'li' , { key: 'D' } , 'D' ) ,
h ( 'li' , { key: 'E' } , 'E' )
] ) ;
btn. onclick = function ( ) {
patch ( myVnode1, myVnode2) ;
}
5.3 diff 处理新旧节点是同一个节点时
流程图
path.js
import vnode from './vnode.js' ;
import createElement from './createElement.js' ;
import patchVnode from './patchVnode.js'
export default function patch ( oldVnode, newVnode ) {
if ( oldVnode. sel == '' || oldVnode. sel == undefined ) {
oldVnode = vnode ( oldVnode. tagName. toLowerCase ( ) , { } , [ ] , undefined , oldVnode) ;
}
if ( oldVnode. key == newVnode. key && oldVnode. sel == newVnode. sel) {
console. log ( '是同一个节点' ) ;
patchVnode ( oldVnode, newVnode) ;
} else {
console. log ( '不是同一个节点,暴力插入新的,删除旧的' ) ;
let newVnodeElm = createElement ( newVnode) ;
if ( oldVnode. elm. parentNode && newVnodeElm) {
oldVnode. elm. parentNode. insertBefore ( newVnodeElm, oldVnode. elm) ;
}
oldVnode. elm. parentNode. removeChild ( oldVnode. elm) ;
}
} ;
import createElement from "./createElement" ;
import updateChildren from './updateChildren.js' ;
export default function patchVnode ( oldVnode, newVnode ) {
if ( oldVnode === newVnode) return ;
if ( newVnode. text != undefined && ( newVnode. children == undefined || newVnode. children. length == 0 ) ) {
console. log ( '新vnode有text属性' ) ;
if ( newVnode. text != oldVnode. text) {
oldVnode. elm. innerText = newVnode. text;
}
} else {
console. log ( '新vnode没有text属性' ) ;
if ( oldVnode. children != undefined && oldVnode. children. length > 0 ) {
updateChildren ( oldVnode. elm, oldVnode. children, newVnode. children) ;
} else {
oldVnode. elm. innerHTML = '' ;
for ( let i = 0 ; i < newVnode. children. length; i++ ) {
let dom = createElement ( newVnode. children[ i] ) ;
oldVnode. elm. appendChild ( dom) ;
}
}
}
}
5.4 手写 diff 更新子节点
5.4.1 新增的情况
新创建的节点(newVnode.children[i].elm)插入到所有未处理的节点(oldVnode.children[um].elm)之前,而不是所有已处理节点之后
h ( 'li' , { key: 'A' } , 'A' )
h ( 'li' , { key: 'B' } , 'B' )
h ( 'li' , { key: 'C' } , 'C' )
h ( 'li' , { key: 'A' } , 'A' )
h ( 'li' , { key: 'B' } , 'B' )
h ( 'li' , { key: 'M' } , 'M' )
h ( 'li' , { key: 'N' } , 'N' )
h ( 'li' , { key: 'C' } , 'C' )
5.4.2 删除的情况
h ( 'li' , { key: 'A' } , 'A' )
h ( 'li' , { key: 'B' } , 'B' )
h ( 'li' , { key: 'C' } , 'C' )
h ( 'li' , { key: 'A' } , 'A' )
h ( 'li' , { key: 'C' } , 'C' )
5.4.3 更新的情况
h ( 'li' , { key: 'A' } , 'A' )
h ( 'li' , { key: 'B' } , 'B' )
h ( 'li' , { key: 'C' } , 'C' )
h ( 'li' , { key: 'A' } , 'A' )
h ( 'li' , { key: 'B' } , 'B' )
h ( 'li' , { key: 'C' } , [
h ( )
] )
5.5 diff 算法的子节点更新策略
5.5.1 经典的 diff 算法优化策略
四种命中查找
新前与旧前
新后与旧后
新后与旧前
此种发生了,涉及移动节点,那么新前指向的节点,移动到旧后之后
新前与旧后
此种发生了,涉及移动节点,那么新前指向的节点,移动到旧前之前
命中一种
没有命中
需要用循环来寻找,移动到 oldStartIdx 之前
5.5.2 新增的情况
如果是旧节点先循环完毕,说明新节点中有要插入的节点
h ( 'li' , { key: 'A' } , 'A' )
h ( 'li' , { key: 'B' } , 'B' )
h ( 'li' , { key: 'C' } , 'C' )
h ( 'li' , { key: 'E' } , 'E' )
h ( 'li' , { key: 'A' } , 'A' )
h ( 'li' , { key: 'B' } , 'B' )
h ( 'li' , { key: 'D' } , 'D' )
h ( 'li' , { key: 'E' } , 'E' )
h ( 'li' , { key: 'C' } , 'C' )
while ( 新前<= 新后&& 旧前<= 旧后) { }
5.5.3 删除的情况
如果是新节点先循环完毕,如果老节点中还有剩余节点,说明他们是要被删除的节点
h ( 'li' , { key: 'A' } , 'A' )
h ( 'li' , { key: 'B' } , 'B' )
h ( 'li' , { key: 'C' } , 'C' )
h ( 'li' , { key: 'D' } , 'D' )
h ( 'li' , { key: 'A' } , 'A' )
h ( 'li' , { key: 'B' } , 'B' )
h ( 'li' , { key: 'D' } , 'D' )
while ( 新前<= 新后&& 旧前<= 旧后) { }
5.5.4 多删除的情况
如果是新节点先循环完毕,如果老节点中还有剩余节点(旧前和新后指针中间的节点),说明他们是要被删除的节点
h ( 'li' , { key: 'A' } , 'A' )
h ( 'li' , { key: 'B' } , 'B' )
h ( 'li' , { key: 'C' } , 'C' )
h ( 'li' , { key: 'E' } , 'E' )
h ( 'li' , { key: 'A' } , 'A' )
h ( 'li' , { key: 'B' } , 'B' )
h ( 'li' , { key: 'D' } , 'D' )
while ( 新前<= 新后&& 旧前<= 旧后) { }
5.5.4 复杂的情况
E
C
M
h ( 'li' , { key: 'A' } , 'A' )
h ( 'li' , { key: 'B' } , 'B' )
undefined
h ( 'li' , { key: 'D' } , 'D' )
undefined
h ( 'li' , { key: 'E' } , 'E' )
h ( 'li' , { key: 'C' } , 'C' )
h ( 'li' , { key: 'M' } , 'M' )
h ( undefined )
h ( undefined )
h ( undefined )
h ( undefined )
h ( undefined )
E
D
C
B
A
h ( 'li' , { key: 'E' } , 'E' )
h ( 'li' , { key: 'D' } , 'D' )
h ( 'li' , { key: 'C' } , 'C' )
h ( 'li' , { key: 'B' } , 'B' )
h ( 'li' , { key: 'A' } , 'A' )
while ( 新前<= 新后&& 旧前<= 旧后) { }
Vue 源码 – 数据响应式原理
1. 相关图解
2. MVVM 模式
2.1 模版
< p> 我{{age}}岁了</ p>
2.2 数据变化
this . age++
2.3 数据变化,视图会自动变化
3. 侵入式和非侵入式
3.1 非侵入式
this . a ++
3.2 侵入式
this . setState ( {
a: this . state. a + 1
} )
this . setData ( {
a: this . data. a + 1
} )
4. Object.defineProperty() 方法
数据劫持 / 数据代理
利用 JavaScript 引擎赋予的功能,检测对象属性变化
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
var obj = { } ;
Object. defineProperty ( obj, 'a' , {
value: 3
} ) ;
Object. defineProperty ( obj, 'b' , {
value: 5
} ) ;
console. log ( obj) ;
console. log ( obj. a, obj. b) ;
Object.defineProperty() 方法可以设置一些额外隐藏的属性
Object. defineProperty ( obj, 'a' , {
value: 3 ,
writable: false
} ) ;
Object. defineProperty ( obj, 'b' , {
value: 5 ,
enumerable: false
} ) ;
5. getter / setter
5.1 get
get 属性的 getter 函数,如果没有 getter,则为 undefined,当访问该属性时,会调用此函数 执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的 this 并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。默认为 undefined。
5.2 set
set 属性的 setter 函数,如果没有 setter,则为 undefined 当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined 注意
Object. defineProperty ( obj, 'a' , {
get ( ) {
console. log ( '你试图访问obj的a属性' ) ;
} ,
set ( ) {
console. log ( '你试图改变obj的a属性' ) ;
}
} ) ;
console. log ( obj. a) ;
obj. a = 10 ;
6. defineReactive 函数
getter / setter 需要变量周转才能工作
var temp;
Object. defineProperty ( obj, 'a' , {
get ( ) {
console. log ( '你试图访问obj的a属性' ) ;
return temp;
} ,
set ( newValue) {
console. log ( '你试图改变obj的a属性' , newValue) ;
temp = newValue;
}
} ) ;
使用 defineReactive 函数不需要设置临时变量,而是用闭包
function defineReactive ( data, key, val ) {
Object. defineProperty ( data, key, {
enumerable: true ,
configurable: true ,
get ( ) {
console. log ( '你试图访问obj的' + key + '属性' ) ;
return val;
} ,
set ( newValue) {
console. log ( '你试图改变obj的'
if ( val === newValue) {
return ;
}
val = newValue;
}
} ) ;
}
7. 递归侦测对象全部属性
7.1 相关图解
7.2 Observer
将一个正常的 object 转换为每个层级的属性都是响应式(可以被侦测的)的 object
var obj = {
a: {
m: {
n: 5
}
} ,
b: 4
} ;
8. 数组的响应式处理
改写七个方法
push pop shift unshift splice sort reverse
9. 依赖收集
9.1 什么是依赖
需要用到数据的地方,称为依赖 Vue1.x,细粒度
依赖,用到数据的 DOM
都是依赖 Vue2.x,中等粒度
依赖,用到数据的组件
是依赖 在 getter 中收集依赖,在 setter 中触发依赖
9.2 Dep 类和 Watcher 类
把依赖收集的代码封装成一个Dep类,它专门用来管理依赖,每个Observer的实例,成员中都有一个Dep的实例
Watcher是一个中介,数据发生变化时通过Watcher中转,通知组件
依赖就是Watcher
只有Watcher触发的getter才会收集依赖\ 哪个Watcher触发了getter,就把哪个Watcher收集到Dep中 Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍 代码实现的巧妙之处:
Watcher把自己设置到全局的一个指定位置,然后读取数据 因为读取了数据,所以会触发这个数据的getter 在getter中就能得到当前正在读取数据的Watcher,并把这个Watcher 收集到Dep中
Vue 源码 – AST 抽象语法树
1. 抽象语法树是什么
< div class = " box" >
< h3 class = " title" > 我是一个标题</ h3>
< ul>
< li v-for = " item in arr" :key = " index" >
{{item}}
</ li>
</ ul>
</ div>
< div class = " box" >
< h3 class = " title" > 我是一个标题</ h3>
< ul>
< li> 牛奶</ li>
< li> 咖啡</ li>
< li> 可乐</ li>
</ ul>
</ div>
2. 抽象语法树本质上就是一个 JS 对象
< div class = " box" >
< h3 class = " title" > 我是一个标题</ h3>
< ul>
< li v-for = " item in arr" :key = " index" >
{{item}}
</ li>
</ ul>
</ div>
{
tag: "div" ,
attrs: [ { name: "class" , value: "box" } ] ,
type: 1 ,
children: [
{
tag: "h3" ,
attrs: [ { name: "class" , value: "title" } ] ,
type: 1 ,
children: [ { text: "我是一个标题" , type: 3 } ]
} ,
{
tag: "ul" ,
attrs: [ ] ,
type: 1 ,
children: [
{
tag: "li" ,
for : "arr" ,
key: "index" ,
alias: "item" ,
type: 1 ,
children: [ ]
}
]
}
]
}
3. 抽象语法树和虚拟节点的关系
相关图解
4. 算法储备
4.1 指针思想
指针就是下标,不是C语言中的指针,C语言中的指针可以操作内存。JS中的指针就是一个下标位置
如果i和j指向的字一样,那么i不动,j后移
如果i和j指向的字不一样,此时说明它们之间的字都是连续相同的,让i追上j, j后移
4.2 递归深入
递归题目1
试输出斐波那契数列的前10项,即1、1、2、3、5、8、13、21、34、55。然后请思 考,代码是否有大量重复的计算?应该如何解决重复计算的问题? cache 思想
{
'0' : 1 ,
'1' : 1 ,
'2' : 2 ,
'3' : 3 ,
'4' : 5 ,
}
递归题目2
形式转换:试将高维数组[1, 2, [3, [4, 5], 6], 7, [8], 9]变为图中所示的对象 只要出现了“规则复现”就要想到用递归
{
children: [
{ value: 1 } ,
{ value: 2 } ,
{ children: [
{ value: 3 } ,
{ children: [
{ value: 4 } ,
{ value: 5 }
] } ,
{ value: 6 }
] } ,
{ value: 7 } ,
{ childrenL [
{ value: 8 } ,
{ value: 9 }
] }
]
}
4.3 栈
栈
栈(stack)又名堆栈,它是一种运算受限的线性表,仅在表尾能进行插入和删除操作
。这一端被称为栈顶
,相对地,把另一端称为栈底
向一个栈插入新元素又称作进栈、入栈或压栈
;从一个栈删除元素又称作出栈或退栈
后进先出(LIFO)
特点:栈中的元素,最先进栈的必定是最后出栈,后进栈的一定会先出栈JavaScript中,栈可以用数组模拟
。需要限制只能使用push()和pop(),不能使用unshift()和shift()。即,数组尾是栈顶 可以用面向对象
等手段,将栈封装的更好 利用“栈”的题目
试编写“智能重复”smartRepeat函数,实现:
将3[abc]变为abcabcabc 将3[2[a]2[b]]变为aabbaabbaabb 将2[1[a]3[b]2[3[c]4[d]]]变为abbbcccddddcccddddabbbcccddddcccdddd 不用考虑输入字符串是非法的情况,比如:
2[a3[b]]是错误的,应该补一个1,即2[1[a]3[b]] [abc]是错误的,应该补一个1,即1[abc] 利用“栈”的题目
遍历每一个字符
如果这个字符是数字,那么就把数字压栈,把空字符串压栈 如果这个字符是字母,那么此时就把栈顶这项改为这个字母 如果这个字符是],那么就将数字弹栈,就把字符串栈的栈顶的元素重复刚刚的这个次数,弹栈,拼接到新栈顶上 遍历每一个字符
如果这个字符是数字,那么就把数字压栈,把空字符串压栈 如果这个字符是字母,那么此时就把栈顶这项改为这个字母 如果这个字符是],那么就将数字弹栈,就把字符串栈的栈顶的元素重复刚刚的这个次数,弹栈,拼接到新栈顶上
5. 手写实现AST抽象语法树