長文警告!閱讀時長5-10m。以代碼爲主,你將瞭解Vue響應式原理和運行機制。前端
原理:vue
<body>
<main>
<input type="text" id="input">
<br/>
<label>值:<span id="span"></span></label>
</main>
<script src="./main.js"></script>
</body>
複製代碼
const obj = {};
const inputDom = document.querySelector('#input');
const spanDom = document.querySelector('#span');
Object.defineProperty(obj, 'txt', {
get() {},
set(newVal) {
inputDom.value = newVal;
spanDom.innerHTML = newVal;
}
})
inputDom.addEventListener('input', (e) => {
obj.txt = e.target.value
})
複製代碼
看看效果node
原理:git
let uid = 0;
class Dep {
constructor() {
this.id = uid++;
this.subs = [];
}
// 添加訂閱者
addSub(sub) {
this.subs.push(sub)
}
// 通知訂閱者更新
notify() {
this.subs.forEach(sub => sub.update())
}
//
depend() {
Dep.target.addDep(this)
// 如果新Dep,則會觸發addSub從新添加訂閱
}
}
// 當指向當前活躍的Watcher => 執行get 便於收集依賴時(排除沒必要要的依賴)
Dep.target = null;
複製代碼
import Dep from './Dep'
class Watcher {
constructor(vm, expOrFn, cb) {
this.depIds = {}; // 存儲訂閱者的id
this.vm = vm; // vue實例
this.expOrFn = expOrFn; // 訂閱數據的key
this.cb = cb; // 數據更新回調
this.val = this.get(); // 首次實例,觸發get,收集依賴
}
get() {
// 當前訂閱者(Watcher)讀取被訂閱數據的值時,通知訂閱者管理員收集當前訂閱者
Dep.target = this;
// 執行一次get
const val = this.vm._data[this.expOrFn];
Dep.target = null;
return val
}
update() {
this.run()
}
run () {
const val = this.get();
if (val !== this.val || isObject(val)) {
this.val = val;
this.cb.call(this.vm, val);
}
}
addDep(dep) {
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this)
this.depIds[dep.id] = dep;
}
}
}
function isObject (obj) {
return obj !== null && typeof obj === 'object'
}
export default Watcher;
複製代碼
class Observer {
constructor(value) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (Array.isArray(value)) {
/// 數組,包裝數組響應式方法
protoAugment(value, arrayMethods)
this.observeArray(value)
} else {
// 對象,遍歷屬性,劫持數據
this.walk(value)
}
}
walk(value) {
Object.keys(value).forEach(key => this.convert(key, value[key]))
}
convert(key, val) {
defineReactive(this.value, key, val)
}
observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
function defineReactive(obj, key, val) {
const dep = new Dep();
// 遞歸添加數據劫持
let chlidOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
dep.depend();
if (chlidOb) {
chlidOb.dep.depend()
if (Array.isArray(val)) {
dependArray(val)
}
}
}
return val
},
set(newVal) {
if (newVal === val) return;
val = newVal;
chlidOb = observe(newVal);
dep.notify()
}
})
}
複製代碼
值得一提的是,defineProperty沒法監聽數組變化,這也是咱們在使用vue初期,困擾的this.arr[index] = xxx不會更新頁面的問題,必須在使用array的方法(經vue包裝過)才能達到預期效果,下面試着改造下Array的方法。github
import { def } from './util'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 會修改原數組的方法
const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
ob.dep.notify()
return result
})
})
複製代碼
實際使用面試
const vm = new Vue({
data: {
txt: '',
arr: []
},
});
inputDom.addEventListener('input', e => vm.txt = e.target.value);
buttonDom.addEventListener('click', e => vm.arr.push(1));
vm.$watch('txt', txt => spanDom.innerHTML = txt);
vm.$watch('arr', arr => span1Dom.innerHTML = arr);
複製代碼
看看效果算法
v2須要開發者操做dom,這一點也不mvvm。向vue看齊,實現一個簡單的模版compiler,處理模版;綁定數據;掛載dom;達到隔離dom操做的效果。小程序
原理:segmentfault
export default function parseHTML(template) {
const box = document.createElement('div')
box.innerHTML = template
const fragment = nodeToFragment(box);
return fragment
}
export function nodeToFragment(el) {
const fragment = document.createDocumentFragment();
let child = el.firstChild;
while (child) {
fragment.appendChild(child);
child = el.firstChild
}
return fragment;
}
複製代碼
export default function patch(el, vm) {
const childNodes = el.childNodes;
[].slice.call(childNodes).forEach(function(node) {
const text = node.textContent;
if (node.nodeType == 1) {
// 元素節點
patchElement(node, vm);
} else if (node.nodeType == 3) {
// 文本節點
patchText(node, vm, text);
}
if (node.childNodes && node.childNodes.length) {
patch(node, vm);
}
});
return el
}
<!--patchElement-->
export default function patchElement(node, vm) {
const nodeAttrs = node.attributes;
const nodeAttrsArr = Array.from(nodeAttrs)
nodeAttrsArr.forEach((attr) => {
const { name, value } = attr;
// 默認指令
if (dirRE.test(name)) {
if (bindRE.test(name)) { // v-bind
const dir = name.replace(bindRE, '')
handleBind(node, vm, value, dir)
} else if (modelRE.test(name)) { // v-model
const dir = name.replace(modelRE, '')
handleModel(node, vm, value, dir)
} else if (onRE.test(name)) { // v-on/@
const dir = name.replace(onRE, '')
handleEvent(node, vm, value, dir)
} else if (ifArr.includes(name)) { // v-if
handleIf(node, vm, value, name)
} else if (forRE.test(name)) { // v-for
handleFor(node, vm, value)
}
node.removeAttribute(name);
}
})
return node
};
<!--patchText-->
const defaultTagRE = /\{\{(.*)\}\}/
export default function patchText(node, vm, text) {
if (defaultTagRE.test(text)) {
const exp = defaultTagRE.exec(text)[1]
const initText = vm[exp];
updateText(node, initText);
new Watcher(vm, exp, (value) => updateText(node, value));
}
}
function updateText(node, value) {
node.textContent = isUndef(value) ? '' : value;
}
複製代碼
<!--v-bind-->
export function handleBind (node, vm, exp, dir) {
const val = vm[exp];
updateAttr(node, val);
new Watcher(vm, exp, (value) => updateAttr(node, value));
}
const updateAttr = (node, attr, value) => node.setAttribute(attr, isUndef(value) ? '' : value);
<!--v-model-->
export function handleModel (node, vm, exp, dir) {
let val = vm[exp];
updateModel(node, val);
new Watcher(vm, exp, (value) => updateModel(node, value));
handleEvent(node, vm, (e) => {
const newValue = e.target.value;
if (val === newValue) return;
vm[exp] = newValue;
val = newValue;
}, 'input')
}
export function handleEvent (node, vm, exp, dir) {
const eventType = dir;
const cb = isFun(exp) ? exp : vm[exp].bind(vm);
if (eventType && cb) {
node.addEventListener(eventType, e => cb(e), false);
}
}
const updateModel = (node, value) => node.value = isUndef(value) ? '' : value;
<!--v-for-->
export function handleFor (node, vm, exp) {
const inMatch = exp.match(forAliasRE)
if (!inMatch) return;
exp = inMatch[2].trim();
const alias = inMatch[1].trim();
const val = vm[exp];
const oldIndex = getIndex(node);
const parentNode = node.parentNode;
parentNode.removeChild(node);
node.removeAttribute('v-for');
const templateNode = node.cloneNode(true);
appendForNode(parentNode, templateNode, val, alias, oldIndex);
new Watcher(vm, exp, (value) => appendForNode(parentNode, templateNode, val, alias, oldIndex));
}
function appendForNode(parentNode, node, arr, alias, oldIndex) {
removeOldNode(parentNode, oldIndex)
for (const key in arr) {
const templateNode = node.cloneNode(true)
const patchNode = patch(templateNode, {[alias]: arr[key]})
patchNode.setAttribute('data-for', true)
parentNode.appendChild(patchNode)
}
}
複製代碼
如今,咱們用模版試下效果數組
let vm = new Vue({
el: '#app',
template:
`<div>
<input v-model="txt" type="text"/>
<input @input="input" type="text"/>
<br />
<label>值:<span>{{txt}}</span></label>
<br />
<button @click="addArr">數組+1</button>
<br />
<label>數組:<span v-for="item in arr">{{item}}</span></label>
<br />
<label v-if="txt">是:<span>{{txt}}</span></label>
<label v-else="txt">否</label>
</div>`,
data: {
txt: '',
arr: [1, 2, 3]
},
methods: {
input(e) {
const newValue = e.target.value;
if (this.txt === newValue) return;
this.txt = newValue;
},
addArr() {
this.arr.push(this.arr.length + 1)
}
}
});
複製代碼
做爲消費級的框架而不是玩具(呵呵!依然是玩具。。。),固然是但願在保證可開發維護同時,咱們的性能要過得去。
顯然,由於數據變化而頻繁地更新dom,不是咱們想要。vue給的方案是VNode(對象的方式描述dom)
原理:
例:
export function generate (ast) {
const code = ast ? genElement(ast) : '_c("div")'
return {
render: `with(this){return ${code}}`
}
}
export function genElement (el) {
if (el.for && !el.forProcessed) {
return genFor(el)
} else if (el.if && !el.ifProcessed) {
return genIf(el)
} else {
let code
let data
if (!el.plain) {
data = genData(el)
}
const children = genChildren(el, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
return code
}
}
....
複製代碼
function updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) { // oldStart == newStart 更新節點
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) { // oldEnd == newEnd 更新節點
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // oldStart == newEnd 更新節點 節點右移
patchVnode(oldStartVnode, newEndVnode)
nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // oldStart == newEnd 更新節點 節點左移
patchVnode(oldEndVnode, newStartVnode)
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode)
oldCh[idxInOld] = undefined
nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 相同的鍵,但不一樣的元素。看成新元素對待
createElm(newStartVnode, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) { // 須要新增節點
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx)
} else if (newStartIdx > newEndIdx) { // 須要移除節點
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
複製代碼
看一下分解動做:
從sameVnode判斷上不難看出,在v-for循環出的列表的場景中,對元素設置key,直接指導diff是否複用DOM。
敲黑板,這裏指出兩個咱們編寫時的問題
寫到這裏一步,完成了vue的基本操縱,剩下擴展component/filter/mixin/生命週期等等特性就不一一分解了。 以上代碼主要爲了描述vue運行過程,部分借鑑vue源碼,但丟失了不少細節,有興趣的同窗能夠參考vue源碼分析。
事實上,虛擬dom的意義遠非提升性能這麼簡單。咱們有了描述UI的規則後,單從vue來說,不依賴常規宿主環境,能夠是瀏覽器,是weex,或者node跑ssr;從大環境來說,這爲原生級跨端提供可能,好比RN;固然也有從編譯上階段實現跨平臺的,好比Taro/uniapp。
關於下一版,參考vue3.x,實現一些新特性。
前面提到Vue2.x採用defineProperty劫持數據,這個作法有兩個問題。
一是須要初始化時,遍歷遞歸一必定義OB;
二是沒法劫持數組的變化,倒不是沒有方案劫持數組,基於性能考量,Vue採用了改造數組方法的方式;
複製代碼
Vue3.0採用了新的劫持方案Proxy,一次性解決上述問題。但就目前國內環境而言,依然存在大量低版本ie用戶,兼容版還會沿用2.x的機制
✅ 解決的問題:
將任意個組件特徵(屬性和方法)拷貝到須要的組件中,達到複用的目的
複製代碼
❌ 形成的困擾:
當多個mixins配合時,會出現數據源不清晰和命名可能衝突的問題
複製代碼
✅ 解決的問題:
讓組件通用功能獲得封裝,而不一樣邏輯經過插槽分發
複製代碼
❌ 形成的困擾:
多層組件嵌套時,沒法清晰的體現具體是哪一個組件在模板中提供哪一個變量。
須要額外實例組件,形成額外性能開銷
複製代碼
✅ 解決的問題:
秉承分層的思想,能夠處理和分發傳入的參數和方法
複製代碼
❌ 形成的困擾:
來自民間的用法,相較與React,Vue的HOC使用起來尤其雞肋。
由於原來的父子組件關係被分割,產生了屬性和方法以及真實ref傳遞問題,好比v-model之類的,都須要高階組件手動處理。
與slot-scope相似,由於須要額外的實例組件而形成性能開銷
複製代碼
✅ 解決的問題(案例)
從官方給出的案例來看,確實不存在上述方案形成的反作用。
至因而否會像社區所反應的,基於函數的 API 會形成大量麪條代碼產生,這就須要你們實踐了才知道了。
複製代碼
如下內容,純屬我的YY,不喜輕噴。
關於下一代,React已經指明瞭一個小方向---Fiber。且先不談它的出現會不會像vdom同樣爲前端帶來革命性的性能提高,單單循環任務調度的思路就很契合js的開發思路,Vue會不會借鑑暫時還不清楚,但至少會有適合Vue的方案出現。
在編譯階段作更多文章,在開發者和機器之間作更多,一方面能讓開發者更加專一邏輯而不是代碼組織;另外一方面提升運行時的效率,借鑑一個現下很熱門的例子---WebAssembly,固然編譯成機器更易於理解和執行的代碼,勢必讓框架編寫更多的判斷來解決適配以及線上調試難以定位等等問題。合理分割compiler和runtime的代碼也是框架必須思考的問題。
而後是Service Worker,目前看真正獲得普遍應用的仍是PWA方面,相信在Google的進一步推廣下(Apple依然會從中做梗),成爲標準也將會在各大框架中獲得應用,好比把diff放到WebWorker中去。這遠比小程序的思路---雙線程要來得有意思的多,固然我仍是尊重小程序做爲平臺向的做用。只是各家小程序接口和質量不一,沒有標準,要坐等小程序消費大戶---JD繼續探索。。。