vue2.0源碼分析之理解響應(yīng)式架構(gòu)
分享前啰嗦
我之前介紹過vue1.0如何實現(xiàn)observer和watcher。本想繼續(xù)寫下去,可是vue2.0橫空出世..所以
直接看vue2.0吧。這篇文章在公司分享過,終于寫出來了。我們采用用最精簡的代碼,還原vue2.0響應(yīng)式架構(gòu)實現(xiàn)
以前寫的那篇 vue 源碼分析之如何實現(xiàn) observer 和 watcher可以作為本次分享的參考。
不過不看也沒關(guān)系,但是***了解下Object.defineProperty
本文分享什么
理解vue2.0的響應(yīng)式架構(gòu),就是下面這張圖
順帶介紹他比react快的其中一個原因
本分實現(xiàn)什么
- const demo = new Vue({
- data: {
- text: "before",
- },
- //對應(yīng)的template 為 <div><span>{{text}}</span></div>
- render(h){
- return h('div', {}, [
- h('span', {}, [this.__toString__(this.text)])
- ])
- }
- })
- setTimeout(function(){
- demo.text = "after"
- }, 3000)
對應(yīng)的虛擬dom會從
<div><span>before</span></div> 變?yōu)? <div><span>after</span></div>
好,開始吧!!!
***步, 講data 下面所有屬性變?yōu)閛bservable
來來來先看代碼吧
- class Vue {
- constructor(options) {
- this.$options = options
- this._data = options.data
- observer(options.data, this._update)
- this._update()
- }
- _update(){
- this.$options.render()
- }
- }
- function observer(value, cb){
- Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb))
- }
- function defineReactive(obj, key, val, cb) {
- Object.defineProperty(obj, key, {
- enumerable: true,
- configurable: true,
- get: ()=>{},
- set:newVal=> {
- cb()
- }
- })
- }
- var demo = new Vue({
- el: '#demo',
- data: {
- text: 123,
- },
- render(){
- console.log("我要render了")
- }
- })
- setTimeout(function(){
- demo._data.text = 444
- }, 3000)
為了好演示我們只考慮最簡單的情況,如果看了vue 源碼分析之如何實現(xiàn) observer 和 watcher可能就會很好理解,不過沒關(guān)系,我們?nèi)詢烧Z再說說,這段代碼要實現(xiàn)的功能就是將
- var demo = new Vue({
- el: '#demo',
- data: {
- text: 123,
- },
- render(){
- console.log("我要render了")
- }
- })
中data 里面所有的屬性置于 observer,然后data里面的屬性,比如 text 以改變,就引起_update()函數(shù)調(diào)用進而重新渲染,是怎樣做到的呢,我們知道其實就是賦值的時候就要改變對吧,當(dāng)我給data下面的text 賦值的時候 set 函數(shù)就會觸發(fā),這個時候 調(diào)用_update 就ok了,但是
- setTimeout(function(){
- demo._data.text = 444
- }, 3000)
demo._data.text沒有demo.text用著爽,沒關(guān)系,我們加一個代理
- _proxy(key) {
- const self = this
- Object.defineProperty(self, key, {
- configurable: true,
- enumerable: true,
- get: function proxyGetter () {
- return self._data[key]
- },
- set: function proxySetter (val) {
- self._data[key] = val
- }
- })
- }
然后在Vue的constructor加上下面這句
- Object.keys(options.data).forEach(key => this._proxy(key))
***步先說到這里,我們會發(fā)現(xiàn)一個問題,data中任何一個屬性的值改變,都會引起
_update的觸發(fā)進而重新渲染,屬性這顯然不夠精準(zhǔn)啊
第二步,詳細闡述***步為什么不夠精準(zhǔn)
比如考慮下面代碼
- new Vue({
- template: `
- <div>
- <section>
- <span>name:</span> {{name}}
- </section>
- <section>
- <span>age:</span> {{age}}
- </section>
- <div>`,
- data: {
- name: 'js',
- age: 24,
- height: 180
- }
- })
- setTimeout(function(){
- demo.height = 181
- }, 3000)
template里面只用到了data上的兩個屬性name和age,但是當(dāng)我改變height的時候,用***步的代碼,會不會觸發(fā)重新渲染?會!,但其實不需要觸發(fā)重新渲染,這就是問題所在!!
第三步,上述問題怎么解決
簡單說說虛擬 DOM
首先,template***都是編譯成render函數(shù)的(具體怎么做,就不展開說了,以后我會說的),然后render 函數(shù)執(zhí)行完就會得到一個虛擬DOM,為了好理解我們寫寫最簡單的虛擬DOM
- function VNode(tag, data, children, text) {
- return {
- tag: tag,
- data: data,
- children: children,
- text: text
- }
- }
- class Vue {
- constructor(options) {
- this.$options = options
- const vdom = this._update()
- console.log(vdom)
- }
- _update() {
- return this._render.call(this)
- }
- _render() {
- const vnode = this.$options.render.call(this)
- return vnode
- }
- __h__(tag, attr, children) {
- return VNode(tag, attr, children.map((child)=>{
- if(typeof child === 'string'){
- return VNode(undefined, undefined, undefined, child)
- }else{
- return child
- }
- }))
- }
- __toString__(val) {
- return val == null ? '' : typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val);
- }
- }
- var demo = new Vue({
- el: '#demo',
- data: {
- text: "before",
- },
- render(){
- return this.__h__('div', {}, [
- this.__h__('span', {}, [this.__toString__(this.text)])
- ])
- }
- })
我們運行一下,他會輸出
- {
- tag: 'div',
- data: {},
- children:[
- {
- tag: 'span',
- data: {},
- children: [
- {
- children: undefined,
- data: undefined,
- tag: undefined,
- text: '' // 正常情況為 字符串 before,因為我們?yōu)榱搜菔揪筒粚懘淼拇a,所以這里為空
- }
- ]
- }
- ]
- }
這就是 虛擬最簡單虛擬DOM,tag是html 標(biāo)簽名,data 是包含諸如 class 和 style 這些標(biāo)簽上的屬性,childen就是子節(jié)點,關(guān)于虛擬DOM就不展開說了。
回到開始的問題,也就是說,我得知道,render 函數(shù)里面依賴了vue實例里面哪些變量(只考慮render 就可以,因為template 也會是幫你編譯成render)。敘述有點拗口,還是看代碼吧
- var demo = new Vue({
- el: '#demo',
- data: {
- text: "before",
- name: "123",
- age: 23
- },
- render(){
- return this.__h__('div', {}, [
- this.__h__('span', {}, [this.__toString__(this.text)])
- ])
- }
- })
就像這段代碼,render 函數(shù)里其實只依賴text,并沒有依賴 name和 age,所以,我們只要text改變的時候
我們自動觸發(fā) render 函數(shù) 讓它生成一個虛擬DOM就ok了(剩下的就是這個虛擬DOM和上個虛擬DOM做比對,然后操作真實DOM,只能以后再說了),那么我們正式考慮一下怎么做
第三步,'touch' 拿到依賴
回到最上面那張圖,我們知道data上的屬性設(shè)置defineReactive后,修改data 上的值會觸發(fā) set。
那么我們?nèi)ata上值是會觸發(fā) get了。
對,我們可以在上面做做手腳,我們先執(zhí)行一下render,我們看看data上哪些屬性觸發(fā)了get,我們豈不是就可以知道 render 會依賴data 上哪些變量了。
然后我么把這些變量做些手腳,每次這些變量變的時候,我們就觸發(fā)render。
上面這些步驟簡單用四個子概括就是 計算依賴。
(其實不僅是render,任何一個變量的改別,是因為別的變量改變引起,都可以用上述方法,也就是computed 和 watch 的原理,也是mobx的核心)
***步,
我們寫一個依賴收集的類,每一個data 上的對象都有可能被render函數(shù)依賴,所以每個屬性在defineReactive
時候就初始化它,簡單來說就是這個樣子的
- class Dep {
- constructor() {
- this.subs = []
- }
- add(cb) {
- this.subs.push(cb)
- }
- notify() {
- console.log(this.subs);
- this.subs.forEach((cb) => cb())
- }
- }
- function defineReactive(obj, key, val, cb) {
- const dep = new Dep()
- Object.defineProperty(obj, key, {
- // 省略
- })
- }
然后,當(dāng)執(zhí)行render 函數(shù)去'touch'依賴的時候,依賴到的變量get就會被執(zhí)行,然后我們就可以把這個 render 函數(shù)加到 subs 里面去了。
當(dāng)我們,set 的時候 我們就執(zhí)行 notify 將所有的subs數(shù)組里的函數(shù)執(zhí)行,其中就包含render 的執(zhí)行。
至此就完成了整個圖,好我們將所有的代碼展示出來
- function VNode(tag, data, children, text) {
- return {
- tag: tag,
- data: data,
- children: children,
- text: text
- }
- }
- class Vue {
- constructor(options) {
- this.$options = options
- this._data = options.data
- Object.keys(options.data).forEach(key => this._proxy(key))
- observer(options.data)
- const vdom = watch(this, this._render.bind(this), this._update.bind(this))
- console.log(vdom)
- }
- _proxy(key) {
- const self = this
- Object.defineProperty(self, key, {
- configurable: true,
- enumerable: true,
- get: function proxyGetter () {
- return self._data[key]
- },
- set: function proxySetter (val) {
- self._data.text = val
- }
- })
- }
- _update() {
- console.log("我需要更新");
- const vdom = this._render.call(this)
- console.log(vdom);
- }
- _render() {
- return this.$options.render.call(this)
- }
- __h__(tag, attr, children) {
- return VNode(tag, attr, children.map((child)=>{
- if(typeof child === 'string'){
- return VNode(undefined, undefined, undefined, child)
- }else{
- return child
- }
- }))
- }
- __toString__(val) {
- return val == null ? '' : typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val);
- }
- }
- function observer(value, cb){
- Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb))
- }
- function defineReactive(obj, key, val, cb) {
- const dep = new Dep()
- Object.defineProperty(obj, key, {
- enumerable: true,
- configurable: true,
- get: ()=>{
- if(Dep.target){
- dep.add(Dep.target)
- }
- return val
- },
- set: newVal => {
- if(newVal === val)
- return
- val = newVal
- dep.notify()
- }
- })
- }
- function watch(vm, exp, cb){
- Dep.target = cb
- return exp()
- }
- class Dep {
- constructor() {
- this.subs = []
- }
- add(cb) {
- this.subs.push(cb)
- }
- notify() {
- this.subs.forEach((cb) => cb())
- }
- }
- Dep.target = null
- var demo = new Vue({
- el: '#demo',
- data: {
- text: "before",
- },
- render(){
- return this.__h__('div', {}, [
- this.__h__('span', {}, [this.__toString__(this.text)])
- ])
- }
- })
- setTimeout(function(){
- demo.text = "after"
- }, 3000)
我們看一下運行結(jié)果
好我們解釋一下 Dep.target 因為我們得區(qū)分是,普通的get,還是在查找依賴的時候的get,
所有我們在查找依賴時候,我們將
- function watch(vm, exp, cb){
- Dep.target = cb
- return exp()
- }
Dep.target 賦值,相當(dāng)于 flag 一下,然后 get 的時候
- get: () => {
- if (Dep.target) {
- dep.add(Dep.target)
- }
- return val
- },
判斷一下,就好了。到現(xiàn)在為止,我們再看那張圖是不是就清楚很多了?
總結(jié)
我非常喜歡,vue2.0 以上代碼為了好展示,都采用最簡單的方式呈現(xiàn)。
不過整個代碼執(zhí)行過程,甚至是命名方式都和vue2.0一樣
對比react,vue2.0 自動幫你監(jiān)測依賴,自動幫你重新渲染,而
react 要實現(xiàn)性能***化,要做大量工作,比如我以前分享的
react如何性能達到***化(前傳),暨react為啥非得使用immutable.js
react 實現(xiàn)pure render的時候,bind(this)隱患。
而 vue2.0 天然幫你做到了***,而且對于像萬年不變的 如標(biāo)簽上靜態(tài)的class屬性,
vue2.0 在重新渲染后做diff 的時候是不比較的,vue2.0比 達到性能***化的react 還要快的一個原因
然后源碼在此,喜歡的記得給個 star 哦😍
后續(xù),我會簡單聊聊,vue2.0的diff。