Vue 源碼解析
Vue 的工作機制
在 new vue()
之后,Vue 會調用進行初始化,會初始化生命周期、事件、props、methods、data、computed和watch等。其中最重要的是通過Object.defineProperty
設置setter
和getter
,用來實現響應式
和依賴收集
。
初始化之后,調用 $mount
掛載組件。
啟動編譯器compile()
,對template進行掃描,parse、optimize、generate,在這個階段會生成渲染函數或更新函數,render function
,生成虛擬節點數,將來我們改變的數據,並不是真的DOM操作,而是虛擬DOM上的數值。
在更新前,會做一個diff算法的比較,通過新值和老值的比較,計算出最小的DOM更新。執行到patch()
來打補丁,做界面更新,目的是用JS計算的時間換DOM操作的時間。因為頁面渲染很耗時間,所以vue的目的就是減少頁面渲染的次數和數量。
render function
除了編譯渲染函數以外,還做了一個依賴搜集(界面中做了很多綁定,如何知道和數據模型之間的關系)。當數據變化時,該去界面中更新哪個數據節點。通過觀察者watcher()
來調用更新函數patch()
編譯
編譯模塊分為三個階段
- parse 使用正則解析template中vue的指令變量等,形成語法樹AST
- optimize 標記一些靜態節點,用作后面的性能優化,在diff的時候直接略過
- generate 把第一步生成的AST轉化為渲染函數render function
響應式
vue 核心內容
初始化的時候通過defineProverty進行綁定,設置通知機制,當編譯生成的渲染函數被實際渲染時,會觸發getter進行依賴收集,在數據變化時,通過setter進行更新。
虛擬DOM
virtual DOM 是react首創,Vue2開始支持,用js對象來描述DOM結構,數據修改的時候,先修改虛擬DOM中的數據,然后數組做diff,最后再匯總所有的diff,力求做最少的dom操作,畢竟js里對比很快,而真實的dom操作太慢。
{
tag: 'div', props: { name: 'xx', style: {color: red}, onClick: xx }, children: [{ tag: 'a', text: 'click me' }] }
<div name="xx" style="color: red" @click="xx"> <a>click me</a> </div>
更新視圖
數據修改觸發setter,然后監聽器會通知進行修改,通過對比兩個DOM樹,得到改變的地方,就是patch,只需要把這些差異修改即可。
Vue2響應式的原理: defineProperty
<div id="app"><div id="name"></div></div> <script> let obj = {} Object.defineProperty(obj, 'name', { get: function() { return document.querySelector('#name').innerHTML }, set: function(val) { document.querySelector('#name').innerHTML = val } }) obj.name='adela' </script>
描述vue數據綁定的原理
利用了Object.defineProperty這個屬性,將data中的每一個屬性,都定義了getter和setter,去監聽這些屬性的變化,當某些屬性變化時,我們可以通知需要更新的地方去更新。[數據劫持]
實現數據響應式
監聽Observe
增加了一個Dep類,用來搜集Watcher對象。
讀數據的時候,會觸發getter函數把當前的Watcher對象(存放在Dep.target中)搜集到Dep類中去。
寫數據的時候,則會觸發setter方法,通知Dep類調用notify來觸發所有watcher對象的update方法更新對應視圖。
編譯Compile
核心邏輯獲取DOM,遍歷DOM,獲取{{}}格式的變量,以及每個DOM的屬性,截獲k-和@開頭的設置響應式。
檢查點
- vue編譯過程是怎么樣的 vue寫的模板語句,HTML不識別,通過編譯的過程,進行依賴搜集,data中的數據模型和視圖進行了綁定,如果模型發生變化,會通知依賴的地方進行更新,這就是執行編譯的目的。模型驅動視圖。
- 雙向綁定的原理是什么 v-model 的指令放在input上,在編譯時,可以解析出v-model。操作時做了兩件事情,一,在當前v-model所屬的元素上加了一個事件監聽,v-model指定的事件回調函數當做input事件回調函數去監聽,當input發生變化時,就將值更新到vue實例上。二、vue實例已經實現了數據的響應化,setter函數會觸發界面中所有依賴的更新。
知識點
let fragment = document.createDocumentFragment();
fragment 是一個指向空DocumentFragment對象的引用。
DocumentFragments 是DOM節點。它們不是主DOM樹的一部分。通常的用例是創建文檔片段,將元素附加到文檔片段,然后將文檔片段附加到DOM樹。在DOM樹中,文檔片段被其所有的子元素所代替。
因為文檔片段存在於內存中,並不在DOM樹中,所以將子元素插入到文檔片段時不會引起頁面回流(對元素位置和幾何上的計算)。因此,使用文檔片段通常會帶來更好的性能。
代碼
kvue.js文件
class KVue { constructor(options) { this.$options = options this.$data = options.data this.observe(this.$data) new Compile(options.el, this) if (options.created) { options.created.call(this) } } observe(value) { if (!value || typeof value !== 'object') { return } Object.keys(value).forEach(key => { this.defineReactive(value, key, value[key]) // 代理data中的屬性到vue實例上 this.proxyData(key) }) } defineReactive(obj, key, val) { this.observe(val) // 遞歸解決數據嵌套 const dep = new Dep() // 初始化dependence Object.defineProperty(obj, key, { get() { Dep.target && dep.addDep(Dep.target) return val }, set(newVal) { if (newVal == val) return val = newVal console.log(`${key}屬性更新了:${val}`) dep.notify() } }) } proxyData(key) { Object.defineProperty(this, key, { get() { return this.$data[key] }, set(newVal) { this.$data[key] = newVal } }) } } // Dep: 用來管理watcher對象。 // 讀數據的時候,會觸發getter函數,把當前的Watcher對象(存放在Dep.target中)搜集到Dep類中去。 // 寫數據的時候,會觸發setter方法,通知Dep類調用notify來觸發所有watcher對象的update方法更新對應視圖。 class Dep { constructor() { // 這里存放若干依賴(watcheer) this.deps = [] } addDep(dep) { this.deps.push(dep) } notify() { // 通知所有的依賴去做更新 this.deps.forEach(dep => dep.update()) } } // Wathcer class Watcher { constructor(vm, key, cb) { this.vm = vm this.key = key this.cb = cb // 將當前watcher實例制定到Dep靜態屬性target Dep.target = this this.vm[this.key] // 觸發getter, 添加依賴 Dep.target = null } update() { console.log(`屬性更新了`) this.cb.call(this.vm, this.vm[this.key]) } }
compile.js文件
class Compile { constructor(el, vm) { // 要遍歷的宿主節點 this.$el = document.querySelector(el) this.$vm = vm // 編譯 if (this.$el) { // 轉換內部內容為片段fragment this.$fragment = this.node2fragment(this.$el) // 執行編譯 this.compile(this.$fragment) // 將編譯完的HTML結果追加至$el this.$el.appendChild(this.$fragment) } } // 將宿主元素中代碼片段拿出來遍歷,比較高效 node2fragment(el) { const frag = document.createDocumentFragment() // 將el中的所有子元素搬家至frag中 let child while ((child = el.firstChild)) { frag.appendChild(child) } return frag } // 編譯過程 compile(el) { const childNodes = el.childNodes Array.from(childNodes).forEach(node => { // 判斷類型 if (this.isElement(node)) { // 元素 // console.log('編譯元素' + node.nodeName) const nodeAttrs = node.attributes Array.from(nodeAttrs).forEach(attr => { const attrName = attr.name // 屬性名 const exp = attr.value // 屬性值 if (this.isDirective(attrName)) { // k-text const dir = attrName .substring(2) this[dir] && this[dir](node, this.$vm, exp) } else if (this.isEvent(attrName)) { let dir = attrName.substring(1) this.eventHandler(node, this.$vm, exp, dir) } }) } else if (this.isInterpolation(node)) { // 文本 // console.log('編譯文本' + node.textContent) this.compileText(node) } // 遞歸子節點 if (node.childNodes && node.childNodes.length > 0) { this.compile(node) } }) } compileText(node) { // console.log(RegExp.$1) this.update(node, this.$vm, RegExp.$1, 'text') } // 更新函數 update(node, vm, exp, dir) { const updaterFn = this[dir + 'Updater'] // 初始化 updaterFn && updaterFn(node, vm[exp]) // 依賴收集 new Watcher(vm, exp, function(value) { updaterFn && updaterFn(node, value) }) } text(node, vm, exp) { this.update(node, vm, exp, 'text') } // 事件處理器 eventHandler(node, vm, exp, dir) { let fn = vm.$options.methods && vm.$options.methods[exp] if (dir && fn) { node.addEventListener(dir, fn.bind(vm)) } } html(node, vm, exp) { this.update(node, vm, exp, 'html') } // 雙向綁定 model(node, vm, exp) { // 指定input的value屬性 this.update(node, vm, exp, 'model') // 視圖對模型響應 node.addEventListener('input', e => { vm[exp] = e.target.value }) } modelUpdater(node, value) { node.value = value } textUpdater(node, value) { node.textContent = value } htmlUpdater(node, value) { node.innerHTML = value } isDirective(attr) { return attr.indexOf('k-') == 0 } isEvent(attr) { return attr.indexOf('@') == 0 } isElement(node) { return node.nodeType === 1 } isInterpolation(node) { return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent) } }
index.html文件
<body> <div id="app"> <p>{{name}}</p> <p k-text="name"></p> <p>{{age}}</p> <p> {{doubleAge}} </p> <input type="text" k-model="name"> <button @click="changeName">click me</button> <div k-html="html"></div> </div> <script src='./compile.js'></script> <script src='./kvue.js'></script> <script> let xx = new KVue({ el: '#app', data: { name: "I am test.", age: 12, html: '<button>這是一個按鈕</button>' }, created() { console.log('開始啦') setTimeout(() => { this.name = '我是測試' }, 1500) }, methods: { changeName() { this.name = '哈嘍,嘻嘻嘻' this.age = 1 this.id = 'xx' console.log(1, this) } } }) </script> </body>