1. DOM基础
DOM是Document Object Model的简写,文档对象模型
DOM是HTML的编程接口
HTML文件和DOM树是一一对应的关系
1.1. DOM基本操作 1.1.1. 查
通过id查找
document.getElementById('')
通过标签名查找
document.getElementsByTagName('')
返回的是NodeList(类数组的对象)
通过class查找
document.getElementsByClass('')
根据传入的css选择器,查找匹配的DOM结点
document.querySelector("p")
只返回第一个匹配到的DOM结点
document.querySelectorAll(".detail")
返回类为detail的所有DOM结点的集合
eg: document.querySelectorAll("ul > li")
; 匹配嵌套在ul中的所有li元素
document.querySelectorAll("img+h3")
;匹配所有紧邻在img后的h3元素
document.querySelectorAll('[data-index]')
; 匹配所有有data-index属性的元素
根据已获取的结点,拿到该节点的子节点或父节点
document.getElementById('').parentNode / nextElementSibling / [0]
1.1.2. 改
改innerHTML
dom.innerHTML = "<strong>修改内容<strong>"
改textContent
dom.textContent = "修改text内容"
修改style值
dom.style = "color: blue"
修改指定属性值
dom.setAttribute("some-attribute", "666")
1.1.3. 增 function getCurrentTimeStr ( ) { const date = new Date (); const hour = date.getHours (); const minutes = date.getMinutes (); const second = date.getSeconds (); return [hour, minutes, second].join (":" ) } let newDom = document .createElement ("p" )newDom.textContent = "页面加载时间为" + getCurrentTimeStr (); document .body .appendChild (newDom)
1.1.4. 删 let liToRemove = document .getElementsByTagName ("li" )[0 ];liToRemove.remove ();
2. DOM事件 2.1. 什么是DOM事件
let liDom = document .getElementById ("something" )liDom.onclick = (event ) => { console .log ('exe!' ) }
页面上的DOM之间是嵌套的结构,如果给内层DOM绑定了事件监听函数,则外层的DOM也会相应触发点击事件
2.2. DOM事件传播
事件捕获阶段 –> 目标事件阶段 –> 事件冒泡阶段
2.2. addEventListener DOM 2级
let liDom = document .getElementById ("something" )liDom.addEventListener ("click" , function (event ){ console .log ('exe!(回调1)' ) }); liDom.addEventListener ("click" , function (event ){ console .log ('exe!(回调2)' ) }, { once : true }); liDom.addEventListener ("click" , function (event ){ console .log ('exe!(回调3)' ) }, { once : true });
和直接onClick相比,可以给一个DOM绑定多个事件
3. BOM Browser Object Model,控制浏览器的行为的接口
3.1. 通过window访问
通过window可以访问浏览器提供的BOM对象
location
navigator
screen
alert、prompt、confirm
localStorage
…
window .alert ("welcome!" ) let res = window ,prompt ("请输入..." , "张三" ) let iinfo = window .navigator .userAgent let res = window .confirm ("是否跳转?" ) if (res) { window .location .href = "http://www.baidu.com" } location.href location.protocol location.host location.pathname location.search location.hash
4. AJAX AJAX是异步的Javascript和XML,可以请求服务端数据,并将数据更新到页面上,且是异步的
4.1. XMLHttpRequest 使用XMLHttpRequest对象与服务器通信
let httpRequest = new XMLHttpRequest ()if (!httpRequest) { alert ("浏览器不支持XMLHttpRequest,如IE!" ) } else { httpRequest.onreadystatechange = function ( ) { if (httpRequest.readyState === httpRequest.DONE ) { if (httpRequest.status === 200 ){ console .log (httpRequest.responseText ) } else { console .log ('error!' ) } } else { console .log ("readyState change: " + httpRequest.readyState ) } } httpRequest.open ( 'GET' , 'url' ) httpRequest.send (); }
4.2. Fetch fetch ('url' ).then (function (response ) { return response.json (); }) .then (function (myJson ) { console .log (myJson) })
5. 架构模式 从一个例子开始…
document .getElementById ('save' ).addEventListener ('click' , () => { let name = document .getElementById ('name' ).value ; let age = document .getElementById ('age' ).value ; let listHtml = ` <tr> <td>${name} </td> <td>${name} </td> </tr>` let tableElement = document .getElementById ('list' ).innerHTML += listHtml }) document .getElementById ('upload' ).addEventListener ('click' , () => { let listElement = document .getElementById ('list' ); let lineElement = document .querySelectorAll ('tr' ); let result = []; lineElement.forEach (item => { let tds = item.querySelectorAll ('td' ); let name = tds[0 ].innerText ; let age = tds[1 ].innerText ; result.push ({name, age}); }) console .log (result) })
修改:维护一个全局result,然后每次增加时添加数据到result,最后导出的时候就不需要从dom中取,直接取result即可
6. MVC 6.1. 架构
Model :比如前文中提到的result全局数组
View
Controller
6.2. 基于MVC的代码重构 let model = []let controller = { add ( ) { let name = document .getElementById ('name' ).value ; let age = document .getElementById ('age' ).value ; model.push ({ name, age }); view.update (); } upload ( ) { console .log (model) } } let view = { update ( ) { let data = model; let listHtml = '' ; data.forEach (({name, age} ) => { listHtml += ` <tr> <td>${name} </td> <td>${name} </td> </tr> ` }) let tableElement = document .getElementById ('list' ).innerHTML = listHtml } } document .getElementById ('save' ).addEventListener ('click' , () => { controller.add (); }) document .getElementById ('upload' ).addEventListener ('click' , () => { controller.upload (); })
再优化:单一职责
如何监听model变化?
观察者模式
addEventListener
就是一个观察者模式
target.addEventListener ('click' , listener); target.dispatchEvent ('click' );
let event = new EventEmitter ();event.on ('addItem' , showList); event.emit ('addItem' );
用观察者模式改写model层
class EventEmitter { constructor ( ) { this ._events = {} } on (evt, listener ) { (this ._event [evt] || (this ._events [evt] = [])).push (listen) } } let event = EventEmmitter ();event.on ('add' , view.update ) let model = { _value = [], push (value ) { this ._value .push (value); event.emit ('add' ); }, get ( ) { return this ._value ; } } let controller = { add ( ) { let name = document .getElementById ('name' ).value ; let age = document .getElementById ('age' ).value ; model.push ({ name, age }); } upload ( ) { console .log (model.get ()) } } let view = { update ( ) { let data = model.get (); let listHtml = '' ; data.forEach (({name, age} ) => { listHtml += ` <tr> <td>${name} </td> <td>${name} </td> </tr> ` }) let tableElement = document .getElementById ('list' ).innerHTML = listHtml } }
7. MVVM
修改ViewModel可以同步更新到View上
MVC:如何更新Model …& 能够观察到Model更新的方法
MVVM:包含数据驱动视图更新的机制 & 渲染视图的模板引擎
7.1. 如何绑定视图和数据的关系
7.2. Vue实现MVVM的思路 7.2.1. 数据劫持
数据劫持 为什么要数据劫持?带着这个问题我们先来看下如何实现数据劫持,在vue2的源码中有个名为defineReactive$$1的方法,该方法就是用来实现数据劫持的,但该方法也只是个壳子,最终实现数据劫持的还是靠的js原生的Object.defineProperty方法 ,这也是vue2死活不支持ie8的原因之一。
Object.defineProperty方法接受三个参数:
第一个参数是被劫持的对象
第二个参数是被劫持的对象中的属性(key)
第三个参数是一个配置项对象(包括:value、enumerable、configurable、get和set等几个属性),如下:
Object .defineProperty (obj,key,{ enumerable : true , configurable : true , get ( ){ }, set ( ){ } })
我们在做数据劫持时主要用到的就是get和set两个属性。
通过该方法,被劫持的对象属性,只要在外界获取或者修改属性值都会触发get或set方法,这样我们就可以在get或set中对属性做一些额外对操作。
了解了数据劫持的实现,也就知道了我们为什么要做数据劫持。是因为我们可以通过数据劫持对数据做一些额外对操作从而实现响应式数据。下面我们以vue的data为例实现一个简单的数据劫持。
function observe (data ){ if ( ({}).toString .call (data) !== '[object Object]' ) return ; const keys = Object .keys (data); keys.foreach (key => { defineReactive$$1 (data, key, data[key]); }) } function defineReactive$$1 (obj, key, val ){ Object .defineProperty (obj, key, { get ( ){ return val; }, set (newV ){ if (newV !== val){ val = newV; } } }) }
7.2.2. 模板编译
为什么要模板编译? 我们知道在vue中是通过一些指令或者小胡子语法来实现数据绑定的,而浏览器并不认识这些指令或者小胡子语法,因此在页面加载后需要将这些语法转换成真正的数据呈现给用户。
下面我们以input元素和v-model指令为例来实现一个简单的模板编译。 本案例中实现模板编译的流程:
遍历#app下所有的节点,然后根据节点的类型做相应的操作
如果是元素节点,获取该节点中所有的属性(attributes)并遍历看是否有v-model指令
如果有v-model指令,则根据该指令绑定的属性名(data中的属性名)获取到对应到值,并赋值给节点的value属性
如果是文本节点,则看该文本内容中是否包含小胡子语法
如果有小胡子语法,同样需要解析出小胡子中绑定的属性名(data中的属性名)并获取到对应到值替换该文本内容
遍历完每个节点后再将该节点作为子节点添加到html到文档碎片中
最后再将整个文档碎片添加到dom中 需要说明到是:在vue中实现是借助虚拟dom实现的,而这里为了简单就借助文档碎片来模拟虚拟dom实现,另外为什么一定要用文档碎片,不能直接遍历节点吗?直接遍历也是可以的但是这样一来由于不停的修改节点势必会造成大量的性能消耗,而通过文档碎片在所有节点遍历完成后只需要一次消耗,这样就大大降低了回流重汇带来的性能损耗。
function nodeTofragment (el, vm ){ let fragment = document .createDocumentFragment (); let child; while (child = el.firstChild ){ compile (child, vm); fragment.appendChild (child); } } function compile (node, vm ){ if (node.nodeType === 1 ){ [...node.attributes ].forEach (item => { if (/^v-/ .test (item.nodeName )){ node.value = vm.$data [item.nodeValue ]; } }); [...node.childNodes ].forEach (item => { compile (item, vm); }); }else if (node.nodeType === 3 ){ if (/\{\{\w+\}\}/ .test (node.textContent )){ node.textContent = node.textContent .replace (/\{\{(\w+)\}\}/ , function (a,b ){ return vm.$data [b]; }) } }
7.2.3. 双向绑定 vue主要是利用数据劫持加发布订阅模式来实现数据的双向绑定的。
在前面数据劫持的时候我们提到,数据劫持的目的就是为了在获取数据或给数据赋值之前对数据做一些额外的操作,那么这些额外的操作其实就是利用发布订阅模式对数据属性进行监控,比如说data中的name属性,首先需要知道这个name属性都在哪里用到了,以便后面如果name值发生改变时及时通知用到name的地方同步更新,这个在vue中叫做依赖收集。怎么才能知道name属性都在哪里用到了呢,这个时候数据劫持就派上用场了,前面说过只要外界对name进行访问都会触发Object.defineProperty中的get函数,那么我们就可以利用这个特点在get函数中对name属性进行监听收集。大概实现思路如下:
首先我们需要定义一个Dep类,用于对属性进行依赖收集和通知用到属性到地方进行同步更新
然后再定义一个Watcher类,用于对属性进行监听,并实现属性值的同步更新
在模板编译的时候,通过watcher来监听属性
在数据劫持的get函数中进行依赖收集
在数据劫持的set函数中通知各个watcher进行数据更新
class Dep { constructor ( ){ this .subs = []; } addSub (sub ){ this .subs .push (sub); } notify ( ){ this .subs .forEach (item => { item.update (); }) } } class Watcher { constructor (node, key, vm ){ Dep .target = this ; this .node = node; this .key = key; this .vm = vm; this .getValue (); Dep .target = null ; } update ( ){ this .getValue (); if (this .node .nodeType === 1 ){ this .node .value = this .value ; }else if (this .node .nodeType === 3 ){ this .node .textContent = this .value ; } } getValue ( ){ this .value = this .vm .$data [this .key ]; } }
7.3. 完整代码 class Dep { constructor ( ) { this .subs = [] } addSubs (sub ) { this .subs .push (sub) } notify ( ) { this .subs .forEach (item => { item.update (); }) } } class Watcher { constructor (node, key, vm ) { Dep .target = this ; this .node = node; this .key = key; this .vm = vm this .getValue (); Dep .target = null ; } update ( ) { this .getValue (); if (this .node .nodeType === 1 ) { this .node .value = this .value ; } else if (this .node .nodeType === 3 ) { this .node .textContent = this .value ; } } getValue ( ) { this .value = this .vm .$data [this .key ]; } } function Vue (options ) { this .$data = options.data ; this .$el = document .querySelector (options.el ); observe (this .$data ) nodeTofragment (this .$el , this ); } const vm = new Vue ({ data : { name : '西瓜watermelon' }, el : '#app' }); function observe (data ) { if (({}).toString .call (data) !== '[object Object]' ) return ; const keys = Object .keys (data); keys.forEach (key => { defineReactive$$1 (data, key, data[key]); }) } function defineReactive$$1 (obj, key, val ) { let dep = new Dep (); Object .defineProperty (obj, key, { get ( ) { if (Dep .target ) { dep.addSubs (Dep .target ); } return val }, set (newV ) { if (newV !== val) { val = newV dep.notify (); } } }); } function nodeTofragment (el, vm ) { let fragment = document .createDocumentFragment (); let child; while (child = el.firstChild ) { compiler (child, vm); fragment.appendChild (child); } el.appendChild (fragment) } function compiler (node, vm ) { if (node.nodeType === 1 ) { [...node.attributes ].forEach (item => { if (/^v-/ .test (item.nodeName )) { new Watcher (node, item.nodeValue , vm) node.value = vm.$data [item.nodeValue ]; node.addEventListener ('input' , () => { vm.$data [item.nodeValue ] = node.value ; }); } }); [...node.childNodes ].forEach (item => { compiler (item, vm); }) } else if (node.nodeType === 3 ) { if (/\{\{\w+\}\}/ .test (node.textContent )) { node.textContent = node.textContent .replace (/\{\{(\w+)\}\}/ , function (a, b ) { new Watcher (node, b, vm) return vm.$data [b] }) } } }
<!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 > 我自己个的vue - mvvm</title > </head > <body > <div id ="app" > <input type ="text" v-model ="name" > <h2 > {{name}}</h2 > </div > </body > </html >
8. 设计模式 8.1. 观察者模式 当对象之间存在一对多的依赖关系时,其中一个对象的状态发生改变,所有依赖它的对象都会收到通知,这就是观察者模式。
在观察者模式中,只有两种主体:目标对象 (Object
) 和 观察者 (Observer
)。宗门任务大殿就是目标对象,弟子们就是观察者。
目标对象 Subject
:
维护观察者列表 observerList
———— 维护拥有订阅权限的弟子列表
定义添加观察者的方法 ———— 提供弟子购买订阅权限的功能
当自身发生变化后,通过调用自己的 notify
方法依次通知每个观察者执行 update
方法 ———— 发布对应任务后通知有订阅权限的弟子
观察者 Observer
需要实现 update
方法,供目标对象调用。update
方法中可以执行自定义的业务逻辑 ———— 弟子们需要定义接收任务通知后的方法,例如去抢任务或任务不适合,继续等待下一个任务
class Observer { constructor (name ) { this .name = name; } update ({taskType, taskInfo} ) { if (taskType === "route" ) { console .log (`${this .name} 不需要日常任务` ); return ; } this .goToTaskHome (taskInfo); } goToTaskHome (info ) { console .log (`${this .name} 去任务大殿抢${info} 任务` ); } } class Subject { constructor ( ) { this .observerList = [] } addObserver (observer ) { this .observerList .push (observer); } notify (task ) { console .log ("发布五星任务" ); this .observerList .forEach (observer => observer.update (task)) } } const subject = new Subject ();const stu1 = new Observer ("弟子1" );const stu2 = new Observer ("弟子2" );subject.addObserver (stu1); subject.addObserver (stu2); const warTask = { taskType : 'war' , taskInfo : "猎杀时刻" } subject.notify (warTask); const routeTask = { taskType : 'route' , taskInfo : "种树浇水" } subject.notify (routeTask);
8.2. 发布订阅模式
基于一个事件(主题)通道,希望接收通知的对象 Subscriber
通过自定义事件订阅主题,被激活事件的对象 Publisher
通过发布主题事件的方式通知各个订阅该主题的 Subscriber
对象。
因此发布订阅模式与观察者模式相比,发布订阅模式中有三个角色,发布者 Publisher
,事件调度中心 Event Channel
,订阅者 Subscriber
。
我们继续以弟子领取任务为栗子,宗门感觉把任务订阅放在任务大殿中有些繁琐,于是决定在任务大殿和弟子中间添加中介 。弟子在中介中订阅其需要的任务类型,当任务大殿发布任务后,中介会将发布任务给对应的订阅者。
class PubSub { constructor ( ) { this .events = {} } subscribe (type, cb ) { if (!this .events [type]) { this .events [type] = []; } this .events [type].push (cb); } publish (type, ...args ) { if (this .events [type]) { this .events [type].forEach (cb => cb (...args)) } } unsubscribe (type, cb ) { if (this .events [type]) { const cbIndex = this .events [type].findIndex (e => e === cb) if (cbIndex != -1 ) { this .events [type].splice (cbIndex, 1 ); } } if (this .events [type].length === 0 ) { delete this .events [type]; } } unsubscribeAll (type ) { if (this .events [type]) { delete this .events [type]; } } } let pubsub = new PubSub ();pubsub.subscribe ('warTask' , function (taskInfo ){ console .log ("宗门殿发布战斗任务,任务信息:" + taskInfo); }) pubsub.subscribe ('routeTask' , function (taskInfo ) { console .log ("宗门殿发布日常任务,任务信息:" + taskInfo); }); pubsub.subscribe ('allTask' , function (taskInfo ) { console .log ("宗门殿发布五星任务,任务信息:" + taskInfo); }); pubsub.publish ('warTask' , "猎杀时刻" ); pubsub.publish ('allTask' , "猎杀时刻" ); pubsub.publish ('routeTask' , "种树浇水" ); pubsub.publish ('allTask' , "种树浇水" );
在发布订阅模式中,发布者和订阅者不知道对方的存在。需要第三方中介,将订阅者和发布者串联起来,利用中介过滤和分配所有输入的消息。也就是说,发布-订阅模式用来处理不同系统组件的信息交流,即使这些组件不知道对方的存在 。
8.3. 观察者模式和发布订阅模式的差异
设计模式
观察者模式
发布订阅模式
主体
Object观察者、Subject目标对象
Publisher发布者、Event Channel事件中心、Subscribe订阅者
主体关系
Subject中通过observerList记录ObServer
Publisher和Subscribe不想不知道对方,通过中介联系
优点
角色明确,Subject和Object要遵循约定的成员方法
松散耦合,灵活度高,通常应用在异步编程中
缺点
紧耦合
当事件类型变多时,会增加维护成本
使用案例
双向数据绑定
事件总线EventBus