模版引擎 数据变为视图的办法
mustache基本使用 mustache官方git: https://github.com/janl/mustache.js
https://www.bootcdn.cn/中查找mustache库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <body> <script type ="text/template" id="template"> <ul> {{#arr}} <li> <div>{{name}}的基本信息</div> <div> <p>姓名:{{name}}</p> <p>年龄:{{age}}</p> </div> </li> {{/arr}} </ul> </script> <script src="jslib/mustache.js"></script> <script type ="text/javascript"> let data ={ arr: [ {name : '张三', age : 18}, {name : '李四', age : 16}, ] } let templateStr = document.getElementById ("template ").innerHTML let domStr = Mustache.render (templateStr , data ) document.querySelector ("body ").innerHTML = domStr </script > </body >
1 2 3 4 5 6 7 8 9 10 11 <div id="box" ></div> <ul id ="ul" > </ul > <script type ="text/javascript" src ="jslib/mustache.js" > </script > <script type ="text/javascript" > let template = `大家好,我是{{name}},今年{{age}}岁` let info = { name : '张三' , age : 18 } document .getElementById ('box' ). innerHTML = Mustache .render (template, info) </script >
1 2 3 4 5 6 7 8 9 10 11 <script type ="text/javascript" > let data ={ arr : ['a' , 'b' , 'c' , {name : '张三' , age : 14 }, [1 ,2 ,3 ]] } let template2 = ` {{#arr}} <li>{{.}}</li> {{/arr}} ` document .getElementById ('ul' ).innerHTML = Mustache .render (template2, data) </script >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <script type="text/javascript" > let data ={ arr : [ {name : '张三' , age : 18 , arr : [1 ,2 ,3 ,4 ]}, {name : '李四' , age : 16 , arr : [4 ,5 ,6 ,7 ]}, ] } let template2 = ` {{#arr}} <li> <div>{{name}}的基本信息</div> <div> <p>姓名:{{name}}</p> <p>年龄:{{age}}</p> <p> 嵌套数组内容: {{#arr}} {{.}} {{/arr}} </p> </div> </li> {{/arr}} ` document .getElementById ('ul' ).innerHTML = Mustache .render (template2, data) </script>
1 2 3 4 5 6 7 8 9 10 11 <script type="text/javascript" > let template3 = ` {{#m}} <h1>hello</h1> {{/m}} ` let obj = { m : false } document .getElementById ('container' ).innerHTML = Mustache .render (template3, obj) </script>
mustache底层核心原理 虽然利用正则表达式可以实现模版中的变量替换,但是无法实现循环遍历,所以并不能用正则表达式来实现模版替换
1 2 3 4 5 6 7 8 9 10 11 12 let templateStr = `hello,大家好,我是{{name}},我今年{{age}}岁了` let data = { name : 'lisi' , age : 13 } function render (templateStr, data ) { return templateStr.replace (/\{\{(\w+)\}\}/g ,function (str, $1 ) { return data[$1 ] }) } console .log (render (templateStr, data))
tokens是一个js的嵌套数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 let obj = { arr :[ {name : 'lisi' , age : 13 , hobby : ['a' , 'b' , 'c' ]}, {name : 'zhangsan' , age : 25 , hobby : [1 , 2 , 3 ]} ] } let template = ` <ul> {{#arr}} <li>{{name}},{{age}}</li> <li> {{#hobby}} <span>{{.}}</span> {{/hobby}} </li> {{/arr}} </ul> ` tokens = [ ['text' ,'\n <ul>\n' , null , null ], ['#' , 'arr' ,null ,null , [ ['text' , '<li>' ,null ,null ], ['name' , 'name' ,null ,null ], ['text' , ',' ,null ,null ], ['name' , 'age' ,null ,null ], ['text' , '</li><li>' ,null ,null ], ['#' , 'hobby' , null . null , [ ['text' , '<span>' ,null ,null ], ['name' , '.' ,null ,null ], ['text' , '</span>' ,null ,null ] ] ], ['text' , '</li>' ,null ,null ] ] ], ['text' , '</ul>' ,null ,null ] ] console .log (Mustache .render (template, data))
手写mustache库 模块化打包工具有webpack(webpack-dev-server) 、rollup 、Parcel 等
• mustache官方库使用rollup 进行模块化打包,而我们今天使用webpack(webpack-dev-server)进行模块化打包,这是因为webpack(webpackdev-server)能让我们更方便地在浏览器中(而不是nodejs环境中)实时调试程序,相比nodejs控制台,浏览器控制台更好用,比如能够点击展开数组的每项。
• 生成库是UMD 的,这意味着它可以同时在nodejs环境中使用,也可以在浏览器环境中使用。实现UMD不难,只需要一个“通用头”即可
UMD 是 JavaScript 模块的通用模块定义模式。这些模块能够在任何地方工作,无论是在客户端、服务器还是其他地方。
UMD 模式通常试图提供与当今最流行的脚本加载器(例如 RequireJS 等)的兼容性。 在许多情况下,它使用 AMD 作为基础,并添加了特殊的外壳来处理 CommonJS 兼容性。
webpack最新版是5,webpack-dev-server最新版是4,但是目前它们的最新版兼容程度不好,建议大家使用这样的版本
初始化 1 2 npm init npm i -D webpack@4 webpack-dev-server@3 webpack-cli@3
新建webpack.config.js文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const path = require ('path' )module .exports = { mode : 'development' , entry : './src/index.js' , output : { filename : 'bundle.js' }, devServer : { contentBase : path.join (__dirname, "www" ), compress : false , port : 8080 , publicPath : "/xuni/" } }
新建src/index.js文件
新建www目录,然后新建index.html
1 2 3 4 5 6 7 8 9 10 11 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Document</title > </head > <body > <h1 > index.html</h1 > <script src ="/xuni/bundle.js" > </script > </body > </html >
修改package.json中的script
运行
然后访问 http://localhost:8080/
扫描类 新建src/Scanner.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 start = this .pos while (!this .eos () && this .tail .indexOf (stopTag) != 0 ) { this .pos ++ this .tail = this .templateStr .substring (this .pos ) } return this .templateStr .substring (start, this .pos ) } eos ( ) { return this .pos >= this .templateStr .length } }
src/index.js 进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import Scanner from './Scanner.js' window .Mustache = { render (templateStr, data ) { const scanner = new Scanner (templateStr) while (!scanner.eos ()) { let word = scanner.scanUtil ("{{" ) console .log (word) scanner.scan ("{{" ) word = scanner.scanUtil ("}}" ) console .log (word) scanner.scan ("}}" ) } } }
要在www/index.html中调用
1 2 3 4 5 6 let templateStr = `hello,大家好,我是{{name}},我今年{{age}}岁了` let data = { name : 'lisi' , age : 13 }Mustache .render (templateStr, data)
生成tokens数组 新建src/parseTemplateToTokens.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import Scanner from './Scanner.js' export default function parseTemplateToTokens (templateStr ) { let tokens = [] const scanner = new Scanner (templateStr) let words while (!scanner.eos ()) { words = scanner.scanUtil ('{{' ) if (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 tokens }
src/index.js测试使用
1 2 3 4 5 6 7 import parseTemplateToTokens from './parseTemplateToTokens.js' window .Mustache = { render (templateStr, data ) { const tokens = parseTemplateToTokens (templateStr) console .log (tokens) } }
目前存在的问题,只是一维数组
将tokens数组改为多维数组 新建src/nestTokens.js文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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) } } return nestedTokens; }
对src/parseTemplateToTokens.js生成的tokens进行进一步解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import Scanner from './Scanner.js' `import nestTokens from './nestTokens.js'` export default function parseTemplateToTokens (templateStr ) { let tokens = [] const scanner = new Scanner (templateStr) let words while (!scanner.eos ()) { words = scanner.scanUtil ('{{' ) if (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)` }
tokens结合数据进行解析 先写一个对根据key值来取对象的值,因为data[‘a.b.c’], []无法获取含有点符号的key值
src/getValueByPointKey.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 export default function getValueByPointKey (dataObj, keyName ) { if (keyName.indexOf ('.' ) != -1 && keyName != '.' ) { let keys = keyName.split ('.' ) let temp = dataObj for (let i = 0 ; i < keys.length ; i++) { temp = temp[keys[i]] } return temp } return dataObj[keyName] }
// 解析tokens数组变为dom字符串
src/renderTemplate.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import getValueByPointKey from './getValueByPointKey.js' import renderTemplateParseArray from './renderTemplateParseArray.js' 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 += token[1 ] } else if (token[0 ] == 'name' ) { resultStr += getValueByPointKey (data, token[1 ]) } else if (token[0 ] == '#' ) { resultStr += renderTemplateParseArray (token, data) } } return resultStr }
但是数组是无法解析,所以在写一个数组解析的文件
/src/renderTemplateParseArray.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import getValueByPointKey from './getValueByPointKey.js' import renderTemplate from './renderTemplate.js' export default function renderTemplateParseArray (token, data ) { let resultStr = '' let arr = getValueByPointKey (data, token[1 ]) for (let i = 0 ; i < arr.length ; i++) { resultStr += renderTemplate (token[2 ], { ...arr[i], '.' : arr[i] }) } return resultStr }
在index.js中调用
1 2 3 4 5 6 7 8 9 import parseTemplateToTokens from './parseTemplateToTokens.js' import renderTemplate from './renderTemplate.js' window .Mustache = { render (templateStr, data ) { const tokens = parseTemplateToTokens (templateStr) const domStr = renderTemplate (tokens, data) return domStr } }
效果如下:
去掉多多余空格 新增处理多余的空格逻辑处理,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 import Scanner from './Scanner.js' import nestTokens from './nestTokens.js' export default function parseTemplateToTokens (templateStr ) { let tokens = [] const scanner = new Scanner (templateStr) let words while (!scanner.eos ()) { words = scanner.scanUtil ('{{' ) if (words != '' ) { ` // 新添加处理空白字符开始 // 标签中的空格不能去掉,比如<div class="box">不能去掉class前面的空格 let isInTag = false // 标记是否在标签里 let _words = '' for (let i = 0; i < words.length; i++) { // 判断是否在标签里 if(words[i] == '<') { isInTag = true } else if (words[i] == '>') { isInTag = false } // 如果这项不是空格, 拼接上 if(!/\s/.test(words[i])) { _words += words[i] } else { // 如果这项是空格,只有当它在标签内的时候,才拼接上 if (isInTag) { _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) }
虚拟DOM和diff算法
diff是发生在虚拟DOM上的
snabbdom snabbdom是著名的虚拟DOM库,是diff算法的鼻祖,Vue源码借鉴了snabbdom;
官方git:https://github.com/snabbdom/snabbdom
新建snabbdom-diff文件夹
snabbdom库是DOM库,当然不能在nodejs环境运行,所以我们需要搭建webpack和webpack-dev-server开发环境,好消息是不需要安装任何loader
• 这里需要注意,必须安装最新版webpack@5,不能安webpack@4,这是因为webpack4没有读取身份证中exports的能力,建议大家使用这样的版本:
1 npm i -D webpack@5 webpack-cli@4 webpack-dev-server@4
修改package.json中的scripts
1 "dev" : "webpack-dev-server"
新建webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const path = require ('path' )module .exports = { mode : 'development' , entry :'./src/index.js' , output :{ filename :'bundle.js' , path : path.resolve (__dirname, 'dist' ), }, devServer :{ static : { directory : path.resolve (__dirname, 'dist' ), }, compress :false , port :8080 } }
新建src/index.js
新建dist/index.html
1 2 3 4 5 6 7 8 9 10 11 12 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Document</title > </head > <body > <h1 > index.html</h1 > <div id ='container' > </div > <script src ="./bundle.js" > </script > </body > </html >
测试配置是否成功
在src/index.js中引入官方示例
注意要现在dist/index.heml中创建一个id为container的容器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import { init, classModule, propsModule, styleModule, eventListenersModule, h, } from "snabbdom" ;const patch = init ([ classModule, propsModule, styleModule, eventListenersModule, ]);const container = document .getElementById ("container" );const vnode = h ("div#container.two.classes" , { on : { click : () => console .log ("div clicked" ) } }, [ h ("span" , { style : { fontWeight : "bold" } }, "This is bold" ), " and this is just normal text" , h ("a" , { props : { href : "/foo" } }, "I'll take you places!" ), ]);patch (container, vnode);const newVnode = h ( "div#container.two.classes" , { on : { click : () => console .log ("updated div clicked" ) } }, [ h ( "span" , { style : { fontWeight : "normal" , fontStyle : "italic" } }, "This is now italic type" ), " and this is still just normal text" , h ("a" , { props : { href : "/bar" } }, "I'll take you places!" ), ] );patch (vnode, newVnode);
http://localhost:8080/
虚拟DOM和h函数 虚拟DOM:用JavaScript对象描述DOM的层次结构。DOM中的一切属性都在虚拟DOM中有对应的属性。
h函数用来产生虚拟节点(vnode) 1 2 const newVnode = h ('a' ,{props : {href : 'http://www.baidu.com' }}, '百度' )console .log (newVnode)
1 2 3 4 5 6 7 8 9 10 { "sel" : "a" , "data" : { "props" : { "href" : "http://www.baidu.com" } } , "text" : "百度" , "elm" : { } }
创建一个虚拟结点,并且渲染到页面上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import { init, classModule, propsModule, styleModule, eventListenersModule, h, } from "snabbdom" ;const patch = init ([ classModule, propsModule, styleModule, eventListenersModule, ]);const newVnode = h ('a' ,{props : {href : 'http://www.baidu.com' , target : '_blank' }}, '百度' )console .log (newVnode)const container = document .getElementById ("container" );patch (container, newVnode);
h函数嵌套 1 2 3 4 5 6 7 8 9 const newVnode2 = h ('ul' ,{},[ h ('li' ,'我是第一个小li' ), h ('li' , [ h ('span' , '我是第二个li,我嵌套了一个span' ) ]), h ('li' ,'我是第三个小li' ) ])console .log (newVnode2)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 { "sel" : "ul" , "data" : { } , "children" : [ { "sel" : "li" , "data" : { } , "text" : "我是第一个小li" , "elm" : { } } , { "sel" : "li" , "data" : { } , "children" : [ { "sel" : "span" , "data" : { } , "text" : "我是第二个li,我嵌套了一个span" , "elm" : { } } ] , "elm" : { } } , { "sel" : "li" , "data" : { } , "text" : "我是第三个小li" , "elm" : { } } ] , "elm" : { } }
手写h函数 src/mysnabbdom/vnode.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export default function vnode (sel, data, children, text, elm ) { const key = data === undefined ? undefined : data.key return { sel, data, children, text, elm, key } }
src/mysnabbdom/h.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import vnode from './vnode.js' export default function h (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 ('传入的参数中某一项不是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 ('传入的参数有误' ) } }
src/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import h from './mysnabbdom/h.js' const newVnode = h ('a' ,{props : {href : 'http://www.baidu.com' , target : '_blank' }}, '百度' )console .log (newVnode)const newVnode2 = h ('ul' ,{},[ h ('li' ,{},'我是第一个小li' ), h ('li' ,{}, [ h ('span' ,{}, '我是第二个li,我嵌套了一个span' ) ]), h ('li' ,{},'我是第三个小li' ) ])console .log (newVnode2)
diff算法
虚拟Dom中的key是这个节点的唯一标识 ,告诉dif算法,在更改前后它们是同一个DOM节点。
只有是同一个虚拟节点,才进行精细化比较 ,否则就是暴力删除旧的、插入新的。
选择器相同并且key值相同就是同一个虚拟结点。(创建元素时支持 div#id.calssName)
只进行同层比较,不会进行跨层比较。 即使是同一片虚拟节点,但是跨层了,对不起,精细化比较不diff你,而是暴力删除旧的、然后插入新的。
diff算法体验 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import { init, classModule, propsModule, styleModule, eventListenersModule, h, } from "snabbdom" const patch = init ([ classModule, propsModule, styleModule, eventListenersModule, ])const container = document .getElementById ("container" )const btn = document .getElementById ("btn" )const ul1 = h ('ul' , [ h ('li' , '1' ), h ('li' , '2' ), h ('li' , '3' ), h ('li' , '4' ) ])patch (container, ul1)const ul2 = h ('ul' , [ h ('li' , '1' ), h ('li' , '2' ), h ('li' , '3' ), h ('li' , '4' ), h ('li' , '5' ) ]) btn.onclick = function ( ) { patch (ul1, ul2) }
手动将第一个li的内容改了以后,然后更换Dom发现只是增加了最后一个<li>5</li>
,并没有全部替换
1 2 3 4 5 6 7 const ul2 = h ('ul' , [ h ('li' , '5' ), h ('li' , '1' ), h ('li' , '2' ), h ('li' , '3' ), h ('li' , '4' ) ])
当我们把5添加到第一个,并且手动修改第一个和第二个li的内容,点击更换DOM之后发现全部被替换了
当我们给虚拟结点加上key之后,就不会全部删除然后替换了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const container = document .getElementById ("container" )const btn = document .getElementById ("btn" )const ul1 = h ('ul' , [ h ('li' , {key : 1 }, '1' ), h ('li' , {key : 2 }, '2' ), h ('li' , {key : 3 }, '3' ), h ('li' , {key : 4 }, '4' ) ])patch (container, ul1)const ul2 = h ('ul' , [ h ('li' , {key : 5 }, '5' ), h ('li' , {key : 1 }, '1' ), h ('li' , {key : 2 }, '2' ), h ('li' , {key : 3 }, '3' ), h ('li' , {key : 4 }, '4' ) ]) btn.onclick = function ( ) { patch (ul1, ul2) }
diff算法处理新旧结点流程
手写diff算法和path函数 前面h函数已经生成了虚拟结点,现在需要把虚拟结点转变为真正的DOM结点
src/mysnabbdom/createElement.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 export default function createElement (vnode ) { let DomNode = document .createElement (vnode.sel ) if (vnode.data && vnode.data .props ) { const keys = Object .keys (vnode.data .props ) for (let i = 0 ; i < keys.length ; i++) { DomNode .setAttribute (keys[i], vnode.data .props [keys[i]]) } } if (vnode.text != '' && (vnode.children == undefined || vnode.children .length == 0 )){ DomNode .innerText = vnode.text } else if (Array .isArray (vnode.children ) && vnode.children .length > 0 ){ for (let i = 0 ; i < vnode.children .length ; i++) { let vnode1 = vnode.children [i] let node = createElement (vnode1) DomNode .appendChild (node) } } vnode.elm = DomNode return vnode.elm }
在src/index.js添加代码测试是否可用(只是测试,后面会在index.js中删除)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import createElement from './mysnabbdom/createElement.js' const newVnode = h ('a' ,{props : {href : 'http://www.baidu.com' , target : '_blank' }}, '百度' )console .log (createElement (newVnode))const newVnode2 = h ('ul' ,{},[ h ('li' ,{},'我是第一个小li' ), h ('li' ,{}, [ h ('span' ,{}, '我是第二个li,我嵌套了一个span' ) ]), h ('li' ,{},'我是第三个小li' ) ])console .log (createElement (newVnode2))
将DOM结点渲染到页面
src/mysnabbdom/patch.js (不过如果是同一结点,还没有解决)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 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 (sameVnode (oldVnode, newVnode)) { console .log ('是同一结点' ) `patchVnode(oldVnode, newVnode)` } else { console .log ('不是同一结点' ) let newVnodeDom = createElement (newVnode) if (oldVnode.elm .parentNode && newVnodeDom) { oldVnode.elm .parentNode .insertBefore (newVnodeDom, oldVnode.elm ) } oldVnode.elm .parentNode .removeChild (oldVnode.elm ) } }function sameVnode (oldVnode, newVnode ) { if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel ) { return true } else { return false } }
同一结点处理流程
对比同一结点
src/mysnabbdom/patchVnode.js (目前还没有做的是两个都有children,就要做到精细化对比)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import createElement from './createElement.js' `import updateChild from './updateChild.js'` export default function patchVnode (oldVnode, newVnode ) { if (oldVnode === newVnode) { console .log ('是同一个对象, 不做处理' ) return } else if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children .length === 0 )) { console .log ('只替换了text' ) if (newVnode.text !== oldVnode.text ) { oldVnode.elm .innerText = newVnode.text } } else { if (oldVnode.children != undefined && oldVnode.children .length > 0 ) { console .log ('新老结点都有children' ) `updateChild(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) } } } }
diff算法 传统更新结点,可能是采用两层for循环遍历来对比有哪些变化,(这种比较麻烦)
diff算法更新策略
四种命中查找:
① 新前 与旧前
② 新后 与旧后
③ 新后 与旧前 (此种发生了,涉及移动节点,那么新后
指向的节点,移动的旧后之后 )
④ 新前 与旧后 (此种发生了,涉及移动节点,那么新前指向的节点,移动的旧前之前 )
命中一种就不再进行命中判断了
如果都没有命中,就需要用循环 来寻找了。移动到oldStartIdx之前。
新增的情况
先执行① 新前与旧前 对比, A和A对比, B和B对比, 到C和D的时候,没命中,就要换②
② 新后与旧后 C和E对比,命不中,换③
③新后与旧前 C和C对比,命中
删除的情况
多删除的情况。
先执行① 新前与旧前 对比, A和A对比, B和B对比, 到C和D的时候,没命中,就要换②
② 新后与旧后 D和E对比,命不中,换③
③新后与旧前 D和C对比,命不中,换④
④新前与旧后 D和E对比,还是命不中
最后只能通过循环遍历,在旧结点中找到D,先将虚拟节点置为undefined,并将结点放在旧前的前面 。然后删除旧前和旧后之间的节点
多删除复杂情况
先执行① 新前与旧前 ,A和E对比,命不中,换②
② 新后与旧后 M和E对比,命不中,换③
③新后与旧前 M和A对比,命不中,换④
④新前与旧后 E和E对比,命中,此时要把旧子节点的E的虚拟结点置为undefined,然后把这个结点移到旧前的前面
–>新前后移指向C
然后 ① 新前与旧前 ,C和A对比,命不中,换②
② 新后和旧后,M和D对比,命不中,换③
③新后和旧前,M和A对比,命不中,换④
④新前和旧后,C和D对比,命不中,这个时候就要循环遍历
找到C后置为undefined,然后把这个结点移到旧前的前面
–>新前后移指向M
然后 ① 新前与旧前 ,M和A对比,命不中,换②
② 新后和旧后,M和D对比,命不中,换③
③新后和旧前,M和A对比,命不中,换④
④新前和旧后,M和D对比,命不中,这个时候就要循环遍历
循环遍历也找不到M,就把M结点移到旧前的前面
最后新前>新后 ,要删除旧前到旧后之间的数据A,B,D
① 新前与旧前 ,E和A对比,命不中,换②
② 新后和旧后,A和E对比,命不中,换③
③新后和旧前,A和A对比,命中
把旧前的A置为undefined,并且把新后指向的结点移到旧后后面
—>新后向上移,旧前向下移
③新后和旧前,B和B对比,命中
把旧前的B置为undefined,并且把新后指向的结点移到旧后后面
….
….
src/mysnabbdom/patchVnode.js (diff算法核心)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 import patchVnode from './patchVnode.js' import createElement from './createElement.js' export default function updateChild (parentELm, oldCh, newCh ) { let newStartIdx = 0 let oldStartIdx = 0 let newEndIdx = newCh.length - 1 let oldEndIdx = oldCh.length - 1 let newStartVnode = newCh[0 ] let oldStartVnode = oldCh[0 ] let newEndVnode = newCh[newEndIdx] let oldEndVnode = oldCh[oldEndIdx] let keyMap = null while (newStartIdx <= newEndIdx && oldStartIdx <= oldEndIdx) { console .log ('☆' ) if (oldStartVnode == null || oldCh[oldStartIdx] == undefined ) { oldStartVnode = oldCh[++oldStartIdx] } else if (newStartVnode == null || newCh[newStartIdx] == undefined ) { newStartVnode = newCh[++newStartIdx] } else if (oldEndVnode == null || oldCh[oldEndIdx] == undefined ) { oldEndVnode = oldCh[--oldEndIdx] } else if (newEndVnode == null || newCh[newEndIdx] == undefined ) { newEndVnode = newCh[--newEndIdx] } else if (checkSameVnode (newStartVnode, oldStartVnode)) { console .log ("命中①:新前与旧前" ) patchVnode (oldStartVnode, newStartVnode) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] }else if (checkSameVnode (newEndVnode, oldEndVnode)) { console .log ("命中②:新后与旧后" ) patchVnode (oldEndVnode, newEndVnode) newEndVnode = newCh[--newEndIdx] oldEndVnode = oldCh[--oldEndIdx] }else if (checkSameVnode (newEndVnode, oldStartVnode)) { console .log ("命中③:新后与旧前" ) patchVnode (oldStartVnode, newEndVnode) parentELm.insertBefore (oldStartVnode.elm , oldEndVnode.elm .nextSibling ) newEndVnode = newCh[--newEndIdx] oldStartVnode = oldCh[++oldStartIdx] } else if (checkSameVnode (newStartVnode, oldEndVnode)) { console .log ("命中④:新前与旧后" ) patchVnode (oldEndVnode, newStartVnode) parentELm.insertBefore (oldEndVnode.elm , oldStartVnode.elm ) newStartVnode = newCh[++newStartIdx] oldEndVnode = oldCh[--oldEndIdx] }else { console .log ("四种都没有命中" ) if (!keyMap) { keyMap = {} for (let i = oldStartIdx; i <= oldEndIdx; i++) { const key = oldCh[i].key if (key != undefined ) { keyMap[key] = i } } } console .log (keyMap) const idxInOld = keyMap[newStartVnode.key ] if (idxInOld == undefined ) { parentELm.insertBefore (createElement (newStartVnode), oldStartVnode.elm ) } else { const oldMoveDom = oldCh[idxInOld] patchVnode (oldMoveDom, newStartVnode) oldCh[idxInOld] = undefined ; parentELm.insertBefore (oldMoveDom, oldStartVnode.elm ) } newStartVnode = newCh[++newStartIdx]; } } if (newStartIdx <= newEndIdx) { console .log ("新结点还有剩余结点,要把所有剩余的节点,都要插入到oldStartIdx之前" ) const before = oldCh[oldStartIdx] == null ? null : oldCh[oldStartIdx].elm for (let i = newStartIdx; i <= newEndIdx; i++) { parentELm.insertBefore (createElement (newCh[i]), before) } }else if (oldStartIdx <= oldEndIdx) { console .log ('old还有剩余节点没有处理,要删除项' ); for (let i = oldStartIdx; i <= oldEndIdx; i++) { if (oldCh[i]){ parentELm.removeChild (oldCh[i].elm ) } } } }function checkSameVnode (a, b ) { if (a.sel === b.sel && a.key === b.key ) { return true } return false }
src/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import h from './mysnabbdom/h.js' import patch from './mysnabbdom/patch.js' const container = document .getElementById ("container" )const btn = document .getElementById ("btn" )const ul1 = h ('ul' , {}, [ h ('li' , {key : 1 }, '1' ), h ('li' , {key : 2 }, '2' ) ])const ul2 = h ('ul' , {}, [ h ('li' , {key : 5 }, '5' ), h ('li' , {key : 1 }, '1' ), h ('li' , {key : 2 }, '2' ) ])patch (container, ul1) btn.onclick = function ( ) { patch (ul1, ul2) }
数据响应式 vue2数据更新原理图
MVVM模式 MVVM简介
数据变化,视图会自动变化
侵入式和非侵入式
侵入性 就是让用户代码产生对框架的依赖,这些代码不能直接脱离框架使用,不利于代码的复用。
非侵入性 就是引入了框架,对现有的类结构没有影响,不需要实现框架某些接口或者特定的类。
Object.defineProperty 数据劫持/ 数据代理
初始化项目
Object.defineProperty方法介绍
Object.defineproperty(obj, prop, descriptor )
第三个参数里面的配置
1 2 3 4 5 6 writable: 是否可重写 value: 当前值get : 读取时内部调用的函数set : 写入时内部调用的函数 enumerable: 是否可以遍历 configurable: 是否可再次修改配置项
数据描述符 还具有以下可选键值:
访问器描述符 还具有以下可选键值:
get
用作属性 getter 的函数,如果没有 getter 则为 undefined
。当访问该属性时,将不带参地调用此函数,并将 this
设置为通过该属性访问的对象(因为可能存在继承关系,这可能不是定义该属性的对象)。返回值将被用作该属性的值。默认值为 undefined
。
set
用作属性 setter 的函数,如果没有 setter 则为 undefined
。当该属性被赋值时,将调用此函数,并带有一个参数(要赋给该属性的值),并将 this
设置为通过该属性分配的对象。默认值为 undefined
。
defineReactive函数
getter/setter需要变量周转才能工作
使用defineReactive函数不需要设置临时变量了,而是用闭包
src/defineReactive.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 export default function defineReactive (obj, key, val ) { if (arguments .length == 2 ) { val = obj[key] } Object .defineProperty (obj, key, { enumerable : true , configurable : true , get ( ) { console .log (`正在访问${obj} 的${key} 属性` ) return val }, set (newValue ) { if (newValue != val) { console .log (`正在修改${obj} 的${key} 属性` ) val = newValue } } }) }
src/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import defineReactive from './defineReactive.js' const obj = { a : { b : { c : { d : 3 } } }, text : 123 }defineReactive (obj, 'a' )defineReactive (obj, 'text' )console .log (obj.a )console .log (obj.text ) obj.text = 123456 obj.a .b .c .d = 1 console .log (obj.a )console .log (obj.text )
递归侦测对象全部属性
Observer类
将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)
的object
src/observe.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import Observer from './Observer.js' export default function (value ) { if (typeof value != 'object' ) { return ; } let ob if (typeof value.__ob__ !== 'undefined' ) { ob = value.__ob__ }else { ob = new Observer (value) } return ob }
src/utils.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export function def (obj, key, value, enumerable ) { Object .defineProperty (obj, key, { value, enumerable, writable : true , configurable : true }) }
src/Observer.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import defineReactive from './defineReactive.js' import { def } from './utils.js' export default class Observer { constructor (value ) { def (value, '__ob__' , this , false ) this .walk (value) } walk (obj ) { for (let key in obj) { defineReactive (obj, key) } } }
src/defineReactive.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import observe from './observe.js' export default function defineReactive (obj, key, val ) { if (arguments .length == 2 ) { val = obj[key] } let childObj = observe (val) Object .defineProperty (obj, key, { enumerable : true , configurable : true , get ( ) { console .log (`正在访问${obj} 下的${key} 属性` ) return val }, set (newValue ) { if (newValue !== val) { console .log (`正在修改${obj} 下的${key} 属性` ) val = newValue childObj = observe (newValue); } } }) }
src/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import observe from './observe.js' const obj = { a : { b : { c : { d : 3 } } }, text : 123 }observe (obj)console .log (obj.a .b )console .log (obj.a .b .c .d ) obj.text = { a :{ d : 1 } }console .log (obj.text .a .d )
数组响应式 vue改写了数组的7个方法,push
, pop
, shift
, unshift
, splice
, sort
, reverse
Object.setPrototypeOf(o, arrayMethods)
设置o的原型为arrayMethods
o.__proto__ = arrayMethods
设置o的原型为arrayMethods
src/array.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import { def } from './utils.js' const arrPrototype = Array .prototype export const arrayMethods = Object .create (arrPrototype)const methods = [ 'push' , 'pop' , 'shift' , 'unshift' , 'splice' , 'sort' , 'reverse' ] methods.forEach (methodsName => { const originMethod = arrPrototype[methodsName] def (arrayMethods, methodsName, function ( ){ const result = originMethod.apply (this , arguments ) const args = [...arguments ] const ob = this .__ob__ let inserted = []; switch (methodsName) { case 'push' : case 'unshift' : inserted = args; break ; case 'splice' : inserted = args.splice (2 ); break ; } if (inserted) { ob.observerArray (inserted) } console .log (`监听到${methodsName} 方法正在改变数组:${this } ` ) return result }, false ) })
src/Observer.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import defineReactive from './defineReactive.js' import { def } from './utils.js' `import { arrayMethods } from './array.js'` import observe from './observe.js' export default class Observer { constructor (value ) { def (value, '__ob__' , this , false ) ` if (Array.isArray(value)) { // 如果是数组,将这个数组的原型,指向arrayMethods Object.setPrototypeOf(value, arrayMethods); this.observerArray(value) } else {` this .walk (value) ` }` } walk (obj ) { for (let key in obj) { defineReactive (obj, key) } } ` // 数组循环遍历 observerArray(arr) { // 这里用个len存储数组的长度,是为了防止数组的长度发生变化 for (let i = 0, len = arr.length; i < len; i++) { observe(arr[i]) } }` }
依赖收集 需要用到数据的地方,称为依赖
• Vue1.x,细粒度依赖,用到数据的DOM 都是依赖;
• Vue2.x,中等粒度依赖,用到数据的组件 是依赖;
• 在getter中收集依赖,在setter中触发依赖
Dep类和Watcher类
把依赖收集的代码封装成一个Dep类,它专门用来管理依赖,每个Observer的实例,
成员中都有一个Dep的实例;
Watcher是一个中介,数据发生变化时通过Watcher中转,通知组件
依赖就是Watcher。只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。
Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。
代码实现的巧妙之处:Watcher把自己设置到全局的一个指定位置,然后读取数据,因为读取了数据,所以会触发这个数据的getter。在getter中就能得到当前正在读取数据的Watcher,并把这个Watcher 收集到Dep中。
完整代码 src/utils.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export function def (obj, key, value, enumerable ) { Object .defineProperty (obj, key, { value, enumerable, writable : true , configurable : true }) }
src/observe.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import Observer from './Observer.js' export default function (value ) { if (typeof value != 'object' ) { return ; } let ob if (typeof value.__ob__ !== 'undefined' ) { ob = value.__ob__ }else { ob = new Observer (value) } return ob }
src/Observer.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import defineReactive from './defineReactive.js' import { def } from './utils.js' import { arrayMethods } from './array.js' import observe from './observe.js' import Dep from './Dep.js' export default class Observer { constructor (value ) { `this.dep = new Dep()` def (value, '__ob__' , this , false ) if (Array .isArray (value)) { Object .setPrototypeOf (value, arrayMethods); this .observerArray (value) } else { this .walk (value) } } walk (obj ) { for (let key in obj) { defineReactive (obj, key) } } observerArray (arr ) { for (let i = 0 , len = arr.length ; i < len; i++) { observe (arr[i]) } } }
src/defineReactive.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import observe from './observe.js' `import Dep from './Dep.js'` export default function defineReactive (obj, key, val ) { `const dep = new Dep()` if (arguments .length == 2 ) { val = obj[key] } let childObj = observe (val) Object .defineProperty (obj, key, { enumerable : true , configurable : true , get ( ) { console .log (`正在访问${obj} 下的${key} 属性` ) ` // 如果现在处于依赖收集阶段 if (Dep.target) { dep.depend() if (childObj) { childObj.dep.depend() } }` return val }, set (newValue ) { if (newValue !== val) { console .log (`正在修改${obj} 下的${key} 属性` ) val = newValue childObj = observe (newValue); dep.notify () } } }) }
src/array.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import { def } from './utils.js' const arrPrototype = Array .prototype export const arrayMethods = Object .create (arrPrototype)const methods = [ 'push' , 'pop' , 'shift' , 'unshift' , 'splice' , 'sort' , 'reverse' ] methods.forEach (methodsName => { const originMethod = arrPrototype[methodsName] def (arrayMethods, methodsName, function ( ){ const result = originMethod.apply (this , arguments ) const args = [...arguments ] const ob = this .__ob__ let inserted = []; switch (methodsName) { case 'push' : case 'unshift' : inserted = args; break ; case 'splice' : inserted = args.splice (2 ); break ; } if (inserted) { ob.observerArray (inserted) } console .log (`监听到${methodsName} 方法正在改变数组:${this } ` ) ob.dep .notify (); return result }, false ) })
src/Dep.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 let uid = 0 export default class Dep { constructor ( ) { console .log ('Dep构造类' ) this .id = uid++ this .subs = [] } addSub (sub ) { this .subs .push (sub) } depend ( ) { if (Dep .target ) { this .addSub (Dep .target ) } } notify ( ) { console .log ('通知方法' ) const subs = this .subs .slice (); for (let i = 0 ; i < subs.length ; i++) { subs[i].update () } } }
src/Watcher.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import Dep from './Dep.js' let uid = 0 export default class Watcher { constructor (target, expression, callback ) { console .log ('Watcher构造方法' ) this .id = uid++ this .target = target this .getter = parsePath (expression) this .callback = callback this .value = this .get () } update ( ) { this .run () } get ( ) { Dep .target = this const obj = this .target let value try { value = this .getter (obj) } finally { Dep .target = null } return value } run ( ) { this .getAndInvoke (this .callback ) } getAndInvoke (cb ) { const value = this .get () if (value !== this .value || typeof value == 'object' ) { const oldValue = this .value this .value = value cb.call (this .target , value, oldValue) } } }function parsePath (str ) { let segments = str.split ('.' ) return (obj ) => { for (let i = 0 ; i < segments.length ; i++) { if (!obj) return obj = obj[segments[i]] } return obj } }
src/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import observe from './observe.js' import Watcher from './Watcher.js' const obj = { a : { b : { c : { d : 3 } } }, text : 123 , arr : [1 , [2 ,3 ], { name : 'zhangsan' }] }observe (obj)new Watcher (obj, 'a.b.c' , (val ) => { console .log ('★我是watcher,我在监控a.m.n' , val) }) obj.a .b .c = 100 console .log (obj)
AST抽象语法树 直接将模版语法编译为HTML语法是非常困难的
抽象语法树本质是一个js对象
抽象语法树和虚拟结点的关系
相关算法储备 指针思想 试寻找字符串中,连续重复次数最多的字符。
‘aaaabbbbbcccccccccccccdddddd’
指针就是下标,不是C语言中的指针,C语言中的指针可以操作内存。JS中的指针
就是一个下标位置。
i: 0
j: 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const str = 'aaaabbbbbcccccccccccccdddddd' function findMaxRepeat (str ) { let i = 0 , j = 1 let max = 0 for (let k = 0 ; k < str.length - 1 ; k++) { if (str[i] !== str[j]) { if (j - i > max) { max = j - i console .log ('max' , max) } i = j } j++ } return max }console .log (findMaxRepeat (str))
递归深入 试输出斐波那契数列的前10项,即1、1、2、3、5、8、13、21、34、55。然后请思考,代码是否有大量重复的计算?应该如何解决重复计算的问题?
cache思想,利用缓存把已经求过的值存起来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 let cache = {}function fib (n ) { console .count ('计数' ) if (n in cache) { console .log ('命中缓存,从缓存中拿' , n, cache[n]) return cache[n] } const v = n == 0 || n == 1 ? 1 : fib (n - 1 ) + fib (n - 2 ) cache[n] = v return v }for (let i = 0 ; i < 10 ; i++) { console .log (fib (i)) }
形式转换:试将高维数组[1, 2, [3, [4, 5], 6], 7, [8], 9]变为图中所示的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 const arr = [1 , 2 , [3 , [4 , 5 ], 6 ], 7 , [8 ], 9 ] function converT (arr ) { let result = [] arr.forEach (item => { if (Array .isArray (item)) { result.push ({ children : converT (item) }) } else { result.push ({ value : item }) } }) return result } console .log (converT (arr)) function converT2 (item ) { if (Array .isArray (item)) { return { children : item.map (_item => converT2 (_item)) } } else { return { value : item } } } console .log (converT2 (arr))
栈
栈(stack)又名堆栈,它是一种运算受限的线性表,仅在表尾能、进行插入和删除操作。这一端被称为栈顶,相对地,把另一端称为栈底。
向一个栈插入新元素又称作进栈、入栈或压栈;从一个栈删除元素 又称作出栈或退栈。
后进先出(LIFO)特点:栈中的元素,最先进栈的必定是最后出栈,后进栈的一定会先出栈
JavaScript中,栈可以用数组模拟。需要限制只能使用push()和pop(),不能使用unshift()和shift()。即,数组尾是栈顶。
题目 试编写“智能重复”smartRepeat函数,实现:
不用考虑输入字符串是非法的情况,比如:
正则相关方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 function smartRepeat (template ) { const stack1 = [] const stack2 = [] let index = 0 while (index < template.length - 1 ) { let rest = template.substring (index) if (/^\d+\[/ .test (rest)) { console .log (stack1, stack2) let num = Number (rest.match (/^(\d+)\[/ )[1 ]) stack1.push (num) stack2.push ('' ) index += (num.toString ().length + 1 ) } else if (/^\w+\]/ .test (rest)) { let word = rest.match (/^(\w+)\]/ )[1 ] stack2[stack2.length - 1 ] = word index += word.length } else if (rest[0 ] == ']' ) { let num = stack1.pop () let word = stack2.pop () stack2[stack2.length - 1 ] += word.repeat (num) index++ } console .log (index, stack1, stack2) } return stack2[0 ].repeat (stack1[0 ]); } str = '3[2[3[a]1[b]]4[d]]' console .log (smartRepeat (str))
手写AST抽象语法树
初始化项目
src/parse.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import parseAttrs from './parseAttrs' export default function parse (htmlStr ) { let index = 0 let rest= '' let tagStartRegExp = /^\<([a-z]+[1-6]?)(\s[^\<]+)?\>/ let tagEndRegExp = /^\<\/([a-z]+[1-6]?)\>/ let wordRegExp = /^([^\<]+)\<\/[a-z]+[1-6]?\>/ const stack1 = [] const stack2 = [{'children' : []}] while (index < htmlStr.length - 1 ) { rest = htmlStr.substring (index) if (tagStartRegExp.test (rest)) { const tag = rest.match (tagStartRegExp)[1 ] let attrsString = rest.match (tagStartRegExp)[2 ] stack1.push (tag) stack2.push ({'tag' : tag, 'children' : [], 'attrs' : parseAttrs (attrsString)}) const attrLength = attrsString ? attrsString.length : 0 index += tag.length + 2 + attrLength } else if (tagEndRegExp.test (rest)) { const tag = rest.match (tagEndRegExp)[1 ] let pop_tag = stack1.pop () if (tag === pop_tag) { let pop_arr = stack2.pop () if (stack2.length > 0 ) { stack2[stack2.length - 1 ].children .push (pop_arr) } } else { throw Error (stack1[stack1.length - 1 ] + '标签匹配错误' ) } index += tag.length + 3 } else if (wordRegExp.test (rest)) { const word = rest.match (wordRegExp)[1 ] if (!/^\s+$/ .test (word)) { stack2[stack2.length - 1 ].children .push ({'text' : word, 'type' : 3 }) } index += word.length } else { index++ } } return stack2[0 ].children [0 ] }
src/parseAttrs.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 export default function parseAttrs (attrsString ) { if (attrsString === undefined ) return [] let isQuotation = false let point = 0 let result = [] for (let i = 0 ; i < attrsString.length ; i++) { let ch = attrsString[i] if (ch == '"' || ch == "'" ) { isQuotation = !isQuotation }else if (ch == ' ' && !isQuotation) { if (!/^\s*$/ .test (attrsString.substring (point, i))) { result.push (attrsString.substring (point, i).trim ()) point = i } } } result.push (attrsString.substring (point).trim ()) result = result.map (item => { const o = item.match (/^(.+)=["'](.+)["']/ ) return { name : o[1 ], value : o[2 ] } }) return result }
src/index.js
1 2 3 4 5 6 7 8 9 10 11 import parse from './parse' const htmlStr = `<div class="aa bb cc" data-n="7" id="mybox"> <h3 name='abc'>你好</h3> <ul> <li>A</li> <li>B</li> <li>C</li> </ul> </div>` const ast = parse (htmlStr)console .log (ast)
指令和生命周期 初始化项目
createDocumentFragment()用法 参考文章:https://blog.csdn.net/weixin_43606809/article/details/97916174
createDocumentFragment()
方法,是用来创建一个虚拟的节点对象,或者说,是用来创建文档碎片节点。它可以包含各种类型的节点,在创建之初是空的。
DocumentFragment
节点不属于文档树,继承的parentNode
属性总是null。它有一个很实用的特点,当请求把一个DocumentFragment
节点插入文档树时,插入的不是DocumentFragment
自身,而是它的所有子孙节点,即插入的是括号里的节点。这个特性使得DocumentFragment
成了占位符,暂时存放那些一次插入文档的节点。它还有利于实现文档的剪切、复制和粘贴操作。
如果使用appendChid
方法将原dom树中的节点添加到DocumentFragment
中时,会删除原来的节点。
另外,当需要添加多个dom元素时,如果先将这些元素添加到DocumentFragment
中,再统一将DocumentFragment
添加到页面,会减少页面渲染dom 的次数,效率会明显提升。
因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。
nodeType nodeType
属性以数字形式返回指定节点的节点类型。
如果节点是元素节点,则 nodeType 属性将返回 1
。
如果节点是属性节点,则 nodeType 属性将返回 2
。
如果节点是文本节点,则 nodeType 属性将返回 3
。
如果节点是注释节点,则 nodeType 属性将返回 8
。
[].slice.call 参考文章:https://blog.csdn.net/qq_23878541/article/details/90413123
1 [].slice === Array .prototype .slice
[]和Array.prototype的区别
自身的属性不同(因为原型与[]的区别)
1 2 3 4 5 Object .getOwnPropertyNames (Array .prototype ) (37 ) ["length" , "constructor" , "concat" , "pop" , "push" , "shift" , "unshift" , "slice" , "splice" , "includes" , "indexOf" , "keys" , "entries" , "forEach" , "filter" , "map" , "every" , "some" , "reduce" , "reduceRight" , "toString" , "toLocaleString" , "join" , "reverse" , "sort" , "lastIndexOf" , "copyWithin" , "find" , "findIndex" , "fill" , "remove" , "removeFirstIf" , "removeIf" , "repeat" , "last" , "lastDef" , "clone" ]Object .getOwnPropertyNames ([]) ["length" ]
所以在本质上[]和Array.prototype没有本质区别,但是调用上是有区别的,但是根据专业检测,[]要更快一点
slice 这个方法在不接受任何参数的时候会返回 this 本身
手写Vue src/Vue.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import observe from './observe' import Compile from './Compile' import Watcher from './Watcher' export default class Vue { constructor (options ) { this .$options = options || {} this ._data = options.data || {} observe (this ._data ) this ._initData () this ._initWatch () new Compile (options.el , this ) } _initData ( ) { let self = this Object .keys (this ._data ).forEach (key => { Object .defineProperty (self, key, { get : () => { return self._data [key] }, set : (newVal ) => { self._data [key] = newVal } }) }) } _initWatch ( ) { let self = this const watch = this .$options .watch Object .keys (watch).forEach (key => { new Watcher (self, key, watch[key]) }) } }
src/Compile.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 import Watcher from './Watcher' export default class Compile { constructor (el, vue ) { console .log ('vue实例被创建了' ) this .$vue = vue this .$el = document .querySelector (el) if (this .$el ) { let $fragment = this .node2Fragment (this .$el ); this .compile ($fragment) this .$el .appendChild ($fragment); } console .log (this .$el , vue) } node2Fragment (el ) { let fragment = document .createDocumentFragment () let child while (child = el.firstChild ) { fragment.appendChild (child) } return fragment } compile (el ) { console .log ('编译' , el) let childNodes = el.childNodes let self = this let reg = /\{\{(.*)\}\}/ childNodes.forEach (node => { let text = node.textContent console .log ('text' , text, node) if (node.nodeType == 1 ) { self.compileElement (node) } else if (node.nodeType == 3 && reg.test (text)) { let name = text.match (reg)[1 ] self.compileText (node, name) } }) } compileElement (node ) { console .log ('元素结点' , node) let nodeAttrs = node.attributes let self = this console .log ('元素上的属性' , nodeAttrs) let reg = /\{\{(.*)\}\}/ let content = node.textContent if (reg.test (content)) { let name = content.match (reg)[1 ] new Watcher (self.$vue , name, name => { node.textContent = name }) let v = self.getVueVal (self.$vue , name) node.textContent = v } const arr = [] arr.slice .call (nodeAttrs).forEach (attr => { let attrName = attr.name let attrValue = attr.value let dir = attrName.substring (2 ) if (attrName.indexOf ('v-' ) == 0 ) { console .log (console .log ('识别到的指令' , dir)) if (dir == 'model' ) { new Watcher (self.$vue , attrValue, attrValue => { node.value = attrValue }) let v = self.getVueVal (self.$vue , attrValue) node.value = v node.addEventListener ('input' , e => { const newVal = e.target .value self.setVueVal (self.$vue , attrValue, newVal) v = newVal }) }else if (dir == 'if' ) { console .log ("if指令" ) } } }) } compileText (node, name ) { node.textContent = this .getVueVal (this .$vue , name) new Watcher (this .$vue , name, value => { node.textContent = value }) } getVueVal (vue, exp ) { let val = vue exp = exp.split ('.' ) exp.forEach (k => { val = val[k] }) return val } setVueVal (vue, exp, value ) { let val = vue exp = exp.split ('.' ) exp.forEach ((k, i ) => { if (i < exp.length - 1 ) { val = val[k] } else { val[k] = value } }) } }
src/index.js
1 2 3 import Vue from './Vue' window .Vue = Vue
www/index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <!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 > <h1 > index.html</h1 > <div id ="app" > 你好{{b.m.n}} <br /> <li > {{a}}</li > <input type ="text" v-model ="b.m.n" > </div > <button onclick ="add()" > 按我加1</button > <script src ="/xuni/bundle.js" > </script > <script > const vm = new Vue ({ el : '#app' , data : { a : 10 , b : { m : { n : 7 } } }, watch : { a ( ) { console .log ('a改变啦' ); } } }) console .log (vm) function add ( ) { vm.b .m .n ++; } </script > </body > </html >
Vue原理 参考文章
template
编译为抽象语法树 AST
用.match()
方法,提取字符串中的关键词,例如标签名div
,Mustache标签对应的变量msg
等
1 2 '<div>{{msg}}</div>' .match (/\{\{((?:.|\r?\n)+?)\}\}/ )
(?:.|\r?\n)
:这是一个非捕获组,用于匹配除换行符以外的任意字符。
获得了"msg"
标签对应变量的名称,我们就能在后续拼接出渲染DOM所需要的_vm.msg
。
即从我们声明的实例new Vue({data() {return {msg: 'abc'}}})
中提取出msg: 'abc'
,渲染为 DOM 节点。
1 2 3 4 5 6 7 new Vue ({ data ( ) { return { msg : 'abc' } } })
<template>
本质上是一段有大量HTML标签的字符串,我们需要遍历获取所有的标签、属性。
遍历方式的逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import parseAttrs from './parseAttrs' export default function parse (htmlStr ) { let index = 0 let tagStartRegExp = /^\<([a-z]+[1-6]?)(\s[^\<]+)?\>/ let tagEndRegExp = /^\<\/([a-z]+[1-6]?)\>/ let wordRegExp = /^([^\<]+)\<\/[a-z]+[1-6]?\>/ vue源码是利用一个栈来存储的 const stack1 = [] const stack2 = [{'children' : []}] while (index < htmlStr.length - 1 ) { rest = htmlStr.substring (index) if (tagStartRegExp.test (rest)) { const tag = rest.match (tagStartRegExp)[1 ] let attrsString = rest.match (tagStartRegExp)[2 ] stack1.push (tag) stack2.push ({'tag' : tag, 'children' : [], 'attrs' : parseAttrs (attrsString)}) const attrLength = attrsString ? attrsString.length : 0 `index += tag.length + 2 + attrLength` } else if (tagEndRegExp.test (rest)) { const tag = rest.match (tagEndRegExp)[1 ] let pop_tag = stack1.pop () if (tag === pop_tag) { let pop_arr = stack2.pop () if (stack2.length > 0 ) { stack2[stack2.length - 1 ].children .push (pop_arr) } } else { throw Error (stack1[stack1.length - 1 ] + '标签匹配错误' ) } index += tag.length + 3 } else if (wordRegExp.test (rest)) { const word = rest.match (wordRegExp)[1 ] if (!/^\s+$/ .test (word)) { stack2[stack2.length - 1 ].children .push ({'text' : word, 'type' : 3 }) } index += word.length } else { index++ } } return stack2[0 ].children [0 ] }
简单来说就是, while(html)
循环,不断的html.match()
提取模板中的信息。然后html = html.substring(n)
删除掉n
个已经遍历过的字符,直到html
字符串为空,表示我们已经遍历、提取了全部的template
。
具体代码实现 1 2 3 4 5 6 7 8 9 10 11 12 class HTMLParser {}class VueCompiler { HTMLParser = new HTMLParser () }class Vue { compiler = new VueCompiler () }
正则表达式补充 参考文章:https://blog.csdn.net/weixin_39359534/article/details/118676496
元字符
描述
\
将下一个字符标记符、或一个向后引用、或一个八进制转义符。例如,“\n”匹配\n。“\n”匹配换行符。序列“\”匹配“\”而“(”则匹配“(”。即相当于多种编程语言中都有的“转义字符”的概念。
^
匹配输入字行首。如果设置了RegExp对象的Multiline属性,^也匹配“\n”或“\r”之后的位置。
$
匹配输入行尾。如果设置了RegExp对象的Multiline属性,$也匹配“\n”或“\r”之前的位置。
*
匹配前面的子表达式任意次 。例如,zo能匹配“z”,也能匹配“zo”以及“zoo”。 等价于{0,}。
+
匹配前面的子表达式一次或多次(大于等于1次) 。例如,“zo+”能匹配“zo”以及“zoo”,但不能匹配“z”。+等价于{1,}。
?
匹配前面的子表达式零次或一次 。例如,“do(es)?”可以匹配“do”或“does”。?等价于{0,1}。
{n }
n 是一个非负整数。匹配确定的n 次。例如,“o{2}”不能匹配“Bob”中的“o”,但是能匹配“food”中的两个o。
{n ,}
n 是一个非负整数。至少匹配n 次。例如,“o{2,}”不能匹配“Bob”中的“o”,但能匹配“foooood”中的所有o。“o{1,}”等价于“o+”。“o{0,}”则等价于“o*”。
{n ,m }
m 和n 均为非负整数,其中n <=m 。最少匹配n 次且最多匹配m 次。例如,“o{1,3}”将匹配“fooooood”中的前三个o为一组,后三个o为一组。“o{0,1}”等价于“o?”。请注意在逗号和两个数之间不能有空格。
?
当该字符紧跟在任何一个其他限制符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的。非贪婪模式尽可能少地匹配所搜索的字符串,而默认的贪婪模式则尽可能多地匹配所搜索的字符串。例如,对于字符串“oooo”,“o+”将尽可能多地匹配“o”,得到结果[“oooo”],而“o+?”将尽可能少地匹配“o”,得到结果 [‘o’, ‘o’, ‘o’, ‘o’]
.
匹配除“\n”和”\r”之外的任何单个字符。要匹配包括“\n”和”\r”在内的任何字符,请使用像“[\s\S]”的模式。
(pattern)
匹配pattern并获取这一匹配。所获取的匹配可以从产生的Matches集合得到,在VBScript中使用SubMatches集合,在JScript中则使用$0…$9属性。要匹配圆括号字符,请使用“”或“”或“”。