开题答辩终于结束了,又要开始我的前端面试学习啦!!!
1.v-model双向绑定原理
class Vue{ constructor(options){ this.$options = options this.$watchEvent = {} if(typeof options.beforeCreate == 'function'){ options.beforeCreate.bind(this)() } // 这是data this.$data = options.data this.proxyData() this.observe() if(typeof options.created == 'function'){ options.beforeCreate.bind(this)() } if(typeof options.beforeMount == 'function'){ options.beforeCreate.bind(this)() } // 这是节点 this.$el = document.querySelector(options.el) // 模板渲染 this.compile(this.$el) if(typeof options.mounted == 'function'){ options.beforeCreate.bind(this)() } } // 1.给vue大对象赋属性,来自于data中 // 2.data中的属性值和vue大对象的属性双向(劫持) proxyData(){ for(let key in this.$data){ Object.defineProperty(this,key,{ get(){ return this.$data[key] }, set(val){ this.$data[key] = val } }) } } // 触发data中的数据发生变化来执行watch中的update observe(){ for(let key in this.$data){ let value = this.$data[key] let that = this Object.defineProperty(this.$data,key,{ get(){ return value }, set(val){ value = val if(that.$watchEvent[key]){ that.$watchEvent[key].forEach((item,index) => { item.update() }) } } }) } } compile(node){ node.childNodes.forEach((item.index) => { // 元素节点 if(item.nodeType == 1){ // 判断元素节点是否绑定了@click if(item.hasAttribute('@click')){ // @click后绑定的属性值 let vmKey = item.getAttribute('@click').trim() item.addEventListener('click',(event) => { this.eventFn = this.$options.methods[vmKey].bind(this) this.eventFn(event) }) } // 判断元素节点是否添加了v-model if(item.hasAttribute('v-model')){ let vmKey = item.getAttribute('v-model').trim(); if(this.hasOwnProperty(vmKey)){ item.value = this[vmKey]; } item.addEventListener('input',(event) => { this[vmKey] = item.value; }) } if(item.childNodes.length>0){ this.compile(item) } } // 这里是文本节点,如果有{{}}就替换成数据 if(item.nodeType == 3){ // 正则匹配 let reg = /\{\{(.*?)\}\}/g let text = item.textContent // 给节点赋值 item.textContent = text.replacce(reg,(match,vmKey) => { vmKey = vmKey.trim() if(this.hasOwnProperty(vmKey)){ let watch = new Watch(this,vmKey,item,'textContent') if(this.$watchEvent[vmKey]){ this.$watchEvent[vmKey].push(watch) }else{ this.$watchEvent[vmKey] = [] this.$watchEvent[vmKey].push(watch) } } return this.$data[vmKey] }) } }) } } class Watch{ constructor(vm,key,node,attr){ // 对象 this.vm = vm // 属性名称 this.key = key // 节点 this.node = node // 改变文本节点内容的字符串 this.attr = attr } //执行改变(update)操作 update(){ this.node[this.attr] = this.vm[this.key] } }
通过Object.defineProperty劫持数据发生的改变,如果数据发生改变了(在set中进行赋值的),触发update方法进行更新节点内容({{ str }}),从而实现了数据的双向绑定原理。
2.diff算法
功能:提升性能
虚拟dom ====> 其实就是数据(把dom数据化)
<script type="text/javascript"> let box = document.getElementById("box"); // 第一种:操作dom console.time('a'); for(let i = 0; i <= 10000; i++){ box.innerHTML = i; } console.timeEnd('a'); // 第二种:数据化 console.time('b'); let num = 0; for(let i = 0; i <= 10000; i++){ num = i; } bpx.innerHTMML = num; console.timeEnd('b'); </script>
对于这两种方法,直接操作dom比较费时,将其数据化可以节省很多时间。
操作dom:73ms,但是数据化:0.28ms
主流:snabbdom、virtual-dom
2.1搭建环境:
npm init -y cnpm install webpack@5 webpack-cli@3 webpack-dev-server@3 -S cnpm install snabbdom -S 新建webpack.config.js 配置webpack.config.js
2.2 虚拟节点和真实节点
虚拟节点:
{ children: undefined data:{} elm:h1 key:undefined sel:"h1" text:"你好h1" }
这是对于h1的虚拟节点,其中虚拟节点在表示时用h('h1',{},"你好h1")
下面就是对于ul的虚拟节点:
{ children:[ 0:{ children:undefined data:{} elm:li key:undefined sel:"li" text:"a" } 1:{...} 2:{...} ] data:{} elm:ul key:undefined sel:"ul" text:undefined }
真实节点:
<h1>你好h1</h1>
2.3 新老节点替换的规则
1、如果新老节点不是同一个节点名称,那么就暴力删除旧的节点,创建插入新的节点
2、只能同级比较,不能跨层比较。如果跨层那么就暴力删除旧的节点,创建插入新的节点。
3、如果是相同节点,又分很多情况
3.1 新节点没有children
如果新的节点没有children,那就证明新的节点是文本,那么直接把旧的替换成新的文本
3.2 新节点有children
新的有children,旧的也有children ===》就是diff算法的核心了
新的有children,旧的没有 ===》创建元素添加(把旧的内容删除清空,增加新的)
***注意:如果要提升性能,一定要加入key,key是唯一标识,在更改前后,确认是不是同一个节点。
const container = document.getElementById("container"); const btn = document.getElementById("btn"); const vnode1 = h('h1',{},'你好'); patch(container,vnode1); const vnode2 = h('div',{},'hi'); btn.onclick = function(){ patch(vnode1,vnode2); }
添加key,这样就可以提升性能:
const container = document.getElementById("container"); const btn = document.getElementById("btn"); const vnode1 = h('ul',{},[ h('li',{key:'a'},'a'), h('li',{key:'b'},'b'), h('li',{key:'c'},'c') ]); patch(container,vnode1); const vnode2 = h('ul',{},[ h('li',{key:'c'},'c'), h('li',{key:'b'},'b'), h('li',{key:'a'},'a') ]); btn.onclick = function(){ patch(vnode1,vnode2); }
3.手写diff算法-生成虚拟dom
index.js
import h from './dom/h' let vnode1 = h('div',{},'你好吖'); console.log(vnode1) -----运行后,应该得到----- { children:undefined data:{} elm:undefined key:undefined sel:"div" text:"你好吖" } ------------------------------- let vnode2 = h('ul',{},[ h('li',{},'a'), h('li',{},'b'), h('li',{},'c') ]) console.log(vnode2) -----运行后,应该得到----- { children:[ 0:{ children:undefined data:{} elm:li key:undefined sel:"li" text:"a" } 1:{...} 2:{...} ] data:{} elm:ul key:undefined sel:"ul" text:undefined } -------------------------------
创建h.js
import vnode from './vnode' export default function(sel, data, params){ // h函数的 第三个参数是字符串类型【意味着:他没有子元素】 if( typeof params == 'string'){ return vnode(undefined, data,undefined, sel, params); }else if(Array.isArray(params)){ // h函数的 第三个参数是数组类型【意味着:他有子元素】 let children = []; for(let item of params){ children.push(item); } return vnode(children, data,undefined, sel, undefined) } }
创建vnode.js
export default function vnode(children, data, elm, sel, text){ return { children, data, elm, sel, text } }
4.手写diff算法-patch不是同一个节点
旧的节点为真实的节点,要将其转换为真实的虚拟节点
//index.html <div id="container"> 这里是container </div> //index.js import h from './dom/h' import patch from './dom/patch' // 获取真实的dom节点 let container = document.getElementById('container'); // 虚拟节点 let vnode1 = h('h1',{},''你好吖); patch(container,vnode1);
//patch.js import vnode from './vnode' export default function(oldVnode, newVnode){ // 如果oldVnode 没有sel,就证明是非虚拟节点(就让他变成虚拟节点) if(oldVnode.sel == undefined){ oldVnode = vnode( [], //children {}, //data oldVnode, //elm oldVnode.tagName.toLowerCase(), //sel undefined // text ) } // 判断 旧的虚拟节点 和 新的虚拟节点 是不是同一个节点 if(oldVnode.sel === newVnode.sel){ // 判断的条件就复杂了(很多了) }else{ // 不是同一个节点,那么就暴力删除旧的节点,创建插入新的节点 // 把新的虚拟节点 创建为 dom节点 let newVnodeElm = createElement(newVnode); // 获取旧的虚拟节点 .elm就是真正节点 let oldVnodeElm = oldVnode.elm; // 创建新的节点 if(newVnodeElm){ oldVnodeElm.parentNode.insertBefore(newVnodeElm,oldVnodeElm); } // 删除旧节点 oldVnodeElm.parentNode.removeChild(oldVnode); } }
// createElement.js // vnode 为新节点,就是要创建的节点 export default function createElement(vnode){ // 创建dom节点 let domNode = document.createElement(vnode.sel); // 判断有没有子节点 children 是不是为undefined if(vnode.children == undefined){ domNode.innerText = vnode.text; } else if(Array.isArray(vnode.children)){ // 说明内部有子节点,需要递归创建节点 for(let child of vnode.children){ let childDom = createElement(child); domNode.appendChild(childDom); } } // 补充elm属性 vnode.elm = domNode; return domNode; }
5.手写diff算法-相同节点有没有children
//patch.js import vnode from './vnode' export default function(oldVnode, newVnode){ // 如果oldVnode 没有sel,就证明是非虚拟节点(就让他变成虚拟节点) if(oldVnode.sel == undefined){ oldVnode = vnode( [], //children {}, //data oldVnode, //elm oldVnode.tagName.toLowerCase(), //sel undefined // text ) } // 判断 旧的虚拟节点 和 新的虚拟节点 是不是同一个节点 if(oldVnode.sel === newVnode.sel){ // 判断的条件就复杂了(很多了) patchVnode(oldVnode,newVnode); }else{ // 不是同一个节点,那么就暴力删除旧的节点,创建插入新的节点 // 把新的虚拟节点 创建为 dom节点 let newVnodeElm = createElement(newVnode); // 获取旧的虚拟节点 .elm就是真正节点 let oldVnodeElm = oldVnode.elm; // 创建新的节点 if(newVnodeElm){ oldVnodeElm.parentNode.insertBefore(newVnodeElm,oldVnodeElm); } // 删除旧节点 oldVnodeElm.parentNode.removeChild(oldVnode); } }
//patchVnode.js import createElement from './createElement' export default function patchVnode(oldVnode,newVnode){ // 判断新节点有没有children if(newVnode.children === undefined){// 没有子节点 // 新的节点的文本 和 旧节点的文本内容是不是一样的 if(newVnode.text !== oldVnode.text){ oldVnode.elm.innerText = newVnode.text } }else{// 新的有子节点 // 新的虚拟节点有, 旧的虚拟节点有 if(oldVnode.children !== undefined && oldVnode.children.length > 0){ // 最复杂的情况了,diff核心了 console.log('新旧节点都有children'); }else{ // 新的虚拟节点有,旧的虚拟节点“没有” // 把旧节点的内容 清空 oldVnode.elm.innerHTML = ''; // 遍历新的 子节点,创建dom元素,添加到页面 for( let child of newVnode.children){ let childDom = createElement(child); oldVnode.elm.appendChild(childDom); } } } }
6.diff算法核心-理论部分
每次都从1开始,不满足就依次往下执行
1.旧前 和新前
匹配:旧前的指针++、新前的指针++
2.旧后 和新后
匹配:旧后的指针--、新后的指针--
3.旧前 和 新后
匹配:旧前的指针++、新后的指针--
4.旧后 和 新前
匹配:旧后的指针--、新前的指针++
5.以上都不满足条件 ===》 查找
新的指针++,新的添加到页面上并且新在旧的节点中有,要给旧的复制成undefined
6.创建或删除
旧的指针指向空,新还有,新就要创建
旧的指针不指向空,新的指针指向空,旧就要删除
注意:若对于旧的指针加或者减指向的是undefined,直接继续加或者减
7.手写diff算法-判断前四种情况
// index.js // 获取到了真实的dom节点 const container = document.getElementById("container"); // 获取到了按钮 const btn = document.getElementById("btn"); // 虚拟节点 const vnode1 = h('ul',{},[ h('li',{key:'a'},'a'), h('li',{key:'b'},'b'), h('li',{key:'c'},'c') ]); patch(container,vnode1); const vnode2 = h('ul',{},[ h('li',{key:'c'},'c'), h('li',{key:'b'},'b'), h('li',{key:'a'},'a') ]); btn.onclick = function(){ patch(vnode1,vnode2); }
// vnode.js export default function vnode(children, data, elm, sel, text){ let key = data.key; return { children, data, elm, key, sel, text } }
//patchVnode.js import createElement from './createElement' import updateChildren from './updateChildren' export default function patchVnode(oldVnode,newVnode){ // 判断新节点有没有children if(newVnode.children === undefined){// 没有子节点 // 新的节点的文本 和 旧节点的文本内容是不是一样的 if(newVnode.text !== oldVnode.text){ oldVnode.elm.innerText = newVnode.text } }else{// 新的有子节点 // 新的虚拟节点有, 旧的虚拟节点有 if(oldVnode.children !== undefined && oldVnode.children.length > 0){ // 最复杂的情况了,diff核心了 console.log('新旧节点都有children'); updateChildren() }else{ // 新的虚拟节点有,旧的虚拟节点“没有” // 把旧节点的内容 清空 oldVnode.elm.innerHTML = ''; // 遍历新的 子节点,创建dom元素,添加到页面 for( let child of newVnode.children){ let childDom = createElement(child); oldVnode.elm.appendChild(childDom); } } } }
// updateChildren.js import patchVnode from './patchVnode' // 判断两个虚拟节点是否为同一个节点 function sameVnode(vnode1,vnode2){ return vnode1.key == vnode2.key; } // 参数一:真实的dom节点 // 参数二:旧的虚拟节点 // 参数三:新的虚拟节点 export default (parentElm, oldCh, newCh) => { let oldStartIdx = 0; //旧前的指针 let oldEndIdx = oldCh.length-1; //旧后的指针 let newStartIdx = 0; //新前的指针 let newEndIdx = newCh.length-1; //新后的指针 let oldStartVnode = oldCh[oldStartIdx]; //旧前虚拟节点 let oldEndVnode = oldCh[oldEndIdx]; //旧后虚拟节点 let newStartVnode = newCh[newStartIdx]; //新前虚拟节点 let newEndVnode = newCh[newEndIdx]; //新后虚拟节点 while( oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){ if( sameVnode(oldStartVnode,newStartVnode)){ // 第一种情况:旧前 和 新前 console.log("1") patchVnode(oldStartVnode,newStartVnode); if(newStartVnode) newStartVnode.elm = oldStartVnode?.elm; oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; }else if(sameVnode(oldEndVnode,newEndVnode)){ // 第二种情况:旧后 和 新后 console.log("2") patchVnode(oldEndVnode,newEndVnode); if(newEndVnode) newEndVnode.elm = oldEndVnode?.elm; oldEndVnode= oldCh[--oldEndIdx]; newEndVnode= newCh[--newEndIdx]; }else if(sameVnode(oldStartVnode,newEndVnode)){ // 第三种情况:旧前 和 新后 console.log("3") patchVnode(oldStartVnode,newEndVnode); if(newEndVnode) newEndVnode.elm = oldStartVnode?.elm; // 把旧前指定的节点移动到旧后指向的节点的后面 parentElm.insertBefore(oldStartVnode.elm,oldEndVnode.elm.nextSibling); oldStartVnode= oldCh[++oldStartIdx]; newEndVnode= newCh[--newEndIdx]; }else if(sameVnode(oldEndVnode,newStartVnode)){ // 第四中情况:旧后 和 新前 console.log("4") patchVnode(oldEndVnode,newStartVnode); if(newStartVnode) newStartVnode.elm = newEndVnode?.elm; // 把旧后指定的节点移动到旧前指向的节点的前面 parentElm.insertBefore(oldEndVnode.elm,oldStartVnode.elm); oldEndVnode= oldCh[--oldEndIdx]; newStartVnode= newCh[++newStartIdx]; }else{ // 第五种情况:以上都不满足条件 ===》 查找 } } }
8.手写diff算法-判断第五种情况
// updateChildren.js import patchVnode from './patchVnode' import createElement from './createElement' // 判断两个虚拟节点是否为同一个节点 function sameVnode(vnode1,vnode2){ return vnode1.key == vnode2.key; } // 参数一:真实的dom节点 // 参数二:旧的虚拟节点 // 参数三:新的虚拟节点 export default (parentElm, oldCh, newCh) => { let oldStartIdx = 0; //旧前的指针 let oldEndIdx = oldCh.length-1; //旧后的指针 let newStartIdx = 0; //新前的指针 let newEndIdx = newCh.length-1; //新后的指针 let oldStartVnode = oldCh[oldStartIdx]; //旧前虚拟节点 let oldEndVnode = oldCh[oldEndIdx]; //旧后虚拟节点 let newStartVnode = newCh[newStartIdx]; //新前虚拟节点 let newEndVnode = newCh[newEndIdx]; //新后虚拟节点 while( oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){ if( oldStartVnode == undefined){ oldStartVnode = oldCh[++oldStartIdx]; }if( oldEndVnode == undefined){ oldEndVnode = oldCh[--oldEndIdx]; }else if( sameVnode(oldStartVnode,newStartVnode)){ // 第一种情况:旧前 和 新前 console.log("1") patchVnode(oldStartVnode,newStartVnode); if(newStartVnode) newStartVnode.elm = oldStartVnode?.elm; oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; }else if(sameVnode(oldEndVnode,newEndVnode)){ // 第二种情况:旧后 和 新后 console.log("2") patchVnode(oldEndVnode,newEndVnode); if(newEndVnode) newEndVnode.elm = oldEndVnode?.elm; oldEndVnode= oldCh[--oldEndIdx]; newEndVnode= newCh[--newEndIdx]; }else if(sameVnode(oldStartVnode,newEndVnode)){ // 第三种情况:旧前 和 新后 console.log("3") patchVnode(oldStartVnode,newEndVnode); if(newEndVnode) newEndVnode.elm = oldStartVnode?.elm; // 把旧前指定的节点移动到旧后指向的节点的后面 parentElm.insertBefore(oldStartVnode.elm,oldEndVnode.elm.nextSibling); oldStartVnode= oldCh[++oldStartIdx]; newEndVnode= newCh[--newEndIdx]; }else if(sameVnode(oldEndVnode,newStartVnode)){ // 第四中情况:旧后 和 新前 console.log("4") patchVnode(oldEndVnode,newStartVnode); if(newStartVnode) newStartVnode.elm = newEndVnode?.elm; // 把旧后指定的节点移动到旧前指向的节点的前面 parentElm.insertBefore(oldEndVnode.elm,oldStartVnode.elm); oldEndVnode= oldCh[--oldEndIdx]; newStartVnode= newCh[++newStartIdx]; }else{ // 第五种情况:以上都不满足条件 ===》 查找 console.log('5'); // 创建一个对象,存虚拟节点的(判断新旧有没有相同节点) const keyMap = {}; for(let i = oldStartIdx;i<=oldEndIdx;i++){ const key = oldCh[i]?.key; if( key ) keyMap[key] = i; } // 在旧节点中寻找新前指向的节点 let idxInOld = keyMap[newStartVnode.key]; // 如果有,说明数据在新旧虚拟节点中都存在 if(idxInOld){ const elmMove = oldCh[idxInOld]; patchVnode(elMove,newStartVnode); // 处理过的节点,在旧虚拟节点的数组中,设置为undefined oldCh[idxInOld] = undefined; parentElm.insertBefore(elMove.elm,oldStartVnode.elm); }else{ // 如果没有找到 ==》 说明是一个新的节点【创建】 parentElm.insertBefore( createElement(newStartVnode),oldStartVnode.elm); } // 新数据(指针) +1 newStartVnode = newCh[++newStartIdx]; } } // 结束while 只有两种情况 (新增和删除) // 1.oldStartIdx > oldEndIdx // 2.newStartIdx > newEndIdx if(oldStartIdx > oldEndIdx){ // 进入新增操作 const before = newCh[newEndIdx+1] ? newCh[newEndIdx+1].elm : null; for( let i=newStartIdx; i<=newEndIdx;i++){ parentElm.insertBefore(createElement(newCh[i],before)); } } else { // 进入删除操作 for(let i=oldStartIdx;i<=oldEndIdx;i++){ parentElm.removeChild(oldCh[i].elm); } } }
9.谈一下MVVM框架
web1.0时代
文件全在一起,也就是前端和后端的代码全在一起
问题:
1.前端和后端都是一个人开发。(技术没有侧重点或者责任不够细分)
2.项目不好维护
3.html、css、js页面的静态内容没有,后端是没有办法工作的(没办法套数据)
web2.0时代
ajax出现了,就可以:前端和后端数据分离了
解决问题:后端不用等前端页面弄完没,后端做后端的事情(写接口),前端布局、特效、发送请求
问题:
1.html、css、js都在一个页面中,单个页面可能内容也比较多的(也会出现不好维护的情况)
出现前端框架MVC、MVVM
解决问题:可以把一个“特别大”页面,进行拆分(组件化),单个组件进行维护
什么是MVVM
Model-View-ViewModel的简写
view:视图【dom ==》 在页面中展示的内容】
model:模型【数据层:vue中的data数据】
viewModel:视图模型层【就是vue源码】
学了快一周的源码了,煎熬