vue實(shí)現(xiàn)原理解析及一步步實(shí)現(xiàn)vue框架

引用:http://www.reibang.com/p/1f9e992eb509

深入解析vue 1實(shí)現(xiàn)原理,并實(shí)現(xiàn)vue雙向數(shù)據(jù)綁定模型vueImitate堤瘤,此模型(vueImitate)只適用于學(xué)習(xí)和了解vue實(shí)現(xiàn)原理杏糙;無(wú)法作為項(xiàng)目中使用慎王,沒(méi)有進(jìn)行任何異常錯(cuò)誤處理及各種使用場(chǎng)景的兼容;但通過(guò)此項(xiàng)目宏侍,可以讓你:

  • 深入了解vue實(shí)現(xiàn)原理
  • 親手一步一步自己實(shí)現(xiàn)vue相應(yīng)功能赖淤,包括雙向綁定、指令如v-model负芋、v-show漫蛔、v-bind等

整體效果如下:

image

下面我們重頭開(kāi)始框架的實(shí)現(xiàn),我們知道旧蛾,vue的使用方式如下:

var vm = new Vue({
    el: 'root',
    data() {
        return {
            message: 'this is test',
            number: 5,
            number1: 1,
            number2: 2,
            showNode: false
        }
    },
    methods: {
        add() {
            this.number1 += 1;
            this.number += 1;
        },
        show() {
            this.showNode = !this.showNode;
        }
    }
})

由此可見(jiàn)莽龟,vue為一個(gè)構(gòu)造函數(shù),并且調(diào)用時(shí)傳入一個(gè)對(duì)象參數(shù)锨天,所以主函數(shù)vueImitate可以如下毯盈,源碼可見(jiàn)這里;并對(duì)參數(shù)進(jìn)行對(duì)應(yīng)的初始化處理:

// init.js 
export default function vueImitate(options) {
    this.options = options || {}; 
    this.selector = options.el ? ('#' + options.el) : 'body'; // 根節(jié)點(diǎn)selector
    this.data = typeof options.data === 'function' ? options.data() : options.data; // 保存?zhèn)魅氲膁ata
    this.el = document.querySelectorAll(this.selector)[0]; // 保存根節(jié)點(diǎn)

    this._directives = [];
}

此時(shí)可以使用new vueImitate(options)的方式進(jìn)行調(diào)用病袄,首先搂赋,我們需要界面上展示正確的數(shù)據(jù),也就是將下面頁(yè)面進(jìn)行處理益缠,使其可以正常訪問(wèn)脑奠;

image

我們可以參考vue的實(shí)現(xiàn)方式,vue將{{ }}這種綁定數(shù)據(jù)的方式轉(zhuǎn)化為指令(directive)幅慌,即v-text類(lèi)似宋欺;而v-text又是如何進(jìn)行數(shù)據(jù)綁定的呢?通過(guò)下面代碼可知,是通過(guò)對(duì)文本節(jié)點(diǎn)重新賦值方式實(shí)現(xiàn)齿诞,源碼見(jiàn)這里

export default {
  bind () {
    this.attr = this.el.nodeType === 3
      ? 'data'
      : 'textContent'
  },
  update (value) {
    this.el[this.attr] = value
  }
}

那么酸休,問(wèn)題來(lái)了,如果需要按照上面的方式實(shí)現(xiàn)數(shù)據(jù)的綁定祷杈,我們需要將現(xiàn)在的字符串{{number}}轉(zhuǎn)化為一個(gè)文本節(jié)點(diǎn)斑司,并對(duì)它進(jìn)行指令化處理;這些其實(shí)也就是vue compile(編譯)但汞、link過(guò)程完成的宿刮,下面我們就先實(shí)現(xiàn)上面功能需求;

compile

整個(gè)編譯過(guò)程肯定從根元素開(kāi)始私蕾,逐步向子節(jié)點(diǎn)延伸處理糙置;

export default function Compile(vueImitate) {
    vueImitate.prototype.compile = function() {
        let nodeLink = compileNode(this.el),
            nodeListLink = compileNodeList(this.el.childNodes, this),
            _dirLength = this._directives.length;

        nodeLink && nodeLink(this);
        nodeListLink && nodeListLink(this);

        let newDirectives = this._directives.slice(_dirLength);

        for(let i = 0, _i = newDirectives.length; i < _i; i++) {
            newDirectives[i]._bind();
        }
    }
}

function compileNode(el) {
    let textLink, elementLink;
    // 編譯文本節(jié)點(diǎn)
    if(el.nodeType === 3 && el.data.trim()) {
        textLink = compileTextNode(el);
    } else if(el.nodeType === 1) {
        elementLink = compileElementNode(el);
    }
    return function(vm) {
        textLink && textLink(vm);
        elementLink && elementLink(vm);
    }
}

function compileNodeList(nodeList, vm) {
    let nodeLinks = [], nodeListLinks = [];
    if(!nodeList || !nodeList.length) {
        return;
    }
    for(let i = 0, _i = nodeList.length; i < _i; i++) {
        let node = nodeList[i];
        nodeLinks.push(compileNode(node)),
        nodeListLinks.push(compileNodeList(node.childNodes, vm));
    }
    return function(vm) {
        if(nodeLinks && nodeLinks.length) {
            for(let i = 0, _i = nodeLinks.length; i < _i; i++) {
                nodeLinks[i] && nodeLinks[i](vm);
            }
        }
        if(nodeListLinks && nodeListLinks.length) {
            for(let i = 0, _i = nodeListLinks.length; i < _i; i++) {
                nodeListLinks[i] && nodeListLinks[i](vm);
            }
        }
    }
}

如上代碼,首先是目,我們通過(guò)定義一個(gè)Compile函數(shù)谤饭,將編譯方法放到構(gòu)造函數(shù)vueImitate.prototype,而方法中懊纳,首先主要使用compileNode編譯根元素揉抵,然后使用compileNodeList(this.el.childNodes, this)編譯根元素下面的子節(jié)點(diǎn);而在compileNodeList中嗤疯,通過(guò)對(duì)子節(jié)點(diǎn)進(jìn)行循環(huán)冤今,繼續(xù)編譯對(duì)應(yīng)節(jié)點(diǎn)及其子節(jié)點(diǎn),如下代碼:

//  function compileNodeList
for(let i = 0, _i = nodeList.length; i < _i; i++) {
    let node = nodeList[i];
    nodeLinks.push(compileNode(node)),
    nodeListLinks.push(compileNodeList(node.childNodes, vm));
}

然后進(jìn)行遞歸調(diào)用茂缚,直到最下層節(jié)點(diǎn):而在對(duì)節(jié)點(diǎn)進(jìn)行處理時(shí)戏罢,主要分為文本節(jié)點(diǎn)和元素節(jié)點(diǎn);文本節(jié)點(diǎn)主要處理上面說(shuō)的{{number}}的編譯脚囊,元素節(jié)點(diǎn)主要處理節(jié)點(diǎn)屬性如v-model龟糕、v-textv-show悔耘、v-bind:click等處理讲岁;

function compileTextNode(el) {
    let tokens = parseText(el.wholeText);
    var frag = document.createDocumentFragment();
    for(let i = 0, _i = tokens.length; i < _i; i++) {
        let token = tokens[i], el = document.createTextNode(token.value)
        frag.appendChild(el);
    }

    return function(vm) {
        var fragClone = frag.cloneNode(true);
        var childNodes = Array.prototype.slice.call(fragClone.childNodes), token;
        for(let j = 0, _j = tokens.length; j < _j; j++) {
            if((token = tokens[j]) && token.tag) {
                let _el = childNodes[j], description = {
                    el: _el,
                    token: tokens[j],
                    def: publicDirectives['text']
                }
                vm._directives.push(new Directive(vm, _el, description))
            }
        }

        // 通過(guò)這兒將`THIS IS TEST {{ number }} test` 這種轉(zhuǎn)化為三個(gè)textNode
        if(tokens.length) {
            replace(el, fragClone);
        }
    }   
}

function compileElementNode(el) {
    let attrs = getAttrs(el);
    return function(vm) {
        if(attrs && attrs.length) {
            attrs.forEach((attr) => {
                let name = attr.name, description, matched;
                if(bindRE.test(attr.name)) {
                    description = {
                        el: el,
                        def: publicDirectives['bind'],
                        name: name.replace(bindRE, ''),
                        value: attr.value
                    }
                } else if((matched = name.match(dirAttrRE))) {
                    description = {
                        el: el,
                        def: publicDirectives[matched[1]],
                        name: matched[1],
                        value: attr.value
                    }
                }
                if(description) {
                    vm._directives.push(new Directive(vm, el, description));

                }
            })
        }
    }
}

這里,先主要說(shuō)明對(duì)文本節(jié)點(diǎn)的處理衬以,我們上面說(shuō)過(guò)缓艳,我們需要對(duì){{number}}之類(lèi)進(jìn)行處理,我們首先必須將其字符串轉(zhuǎn)化為文本節(jié)點(diǎn)看峻,如this is number1: {{number1}}這種阶淘,我們必須轉(zhuǎn)換為兩個(gè)文本節(jié)點(diǎn),一個(gè)是this is number1:互妓,它不需要進(jìn)行任何處理溪窒;另一個(gè)是{{number1}}分井,它需要進(jìn)行數(shù)據(jù)綁定,并實(shí)現(xiàn)雙向綁定霉猛;因?yàn)橹挥修D(zhuǎn)化為文本節(jié)點(diǎn),才能使用v-text類(lèi)似功能實(shí)現(xiàn)數(shù)據(jù)的綁定珠闰;而如何進(jìn)行將字符串文本分割為不同的文本節(jié)點(diǎn)呢惜浅,那么,就只能使用正則方式let reg = /\{\{(.+?)\}\}/ig;{{ number }}這種形式數(shù)據(jù)與普通正常文本分割之后伏嗜,再分別創(chuàng)建textNode坛悉,如下:

function parseText(str) {
    let reg = /\{\{(.+?)\}\}/ig;
    let matchs = str.match(reg), match, tokens = [], index, lastIndex = 0;

    while (match = reg.exec(str)) {
        index = match.index
        if (index > lastIndex) {
          tokens.push({
            value: str.slice(lastIndex, index)
          })
        }
        tokens.push({
            value: match[1],
            html: match[0],
            tag: true
        })
        lastIndex = index + match[0].length
    }

    return tokens;
}

通過(guò)上面parseText方法,可以將this is number: {{number}}轉(zhuǎn)化為如下結(jié)果:

image

轉(zhuǎn)化為上圖結(jié)果后承绸,就對(duì)返回?cái)?shù)組進(jìn)行循環(huán)裸影,分別通過(guò)創(chuàng)建文本節(jié)點(diǎn);這兒為了性能優(yōu)化,先創(chuàng)建文檔碎片军熏,將節(jié)點(diǎn)放入文檔碎片中轩猩;

// function compileTextNode
// el.wholeText => 'this is number: {{number}}'

let tokens = parseText(el.wholeText);
var frag = document.createDocumentFragment();
for(let i = 0, _i = tokens.length; i < _i; i++) {
    let token = tokens[i], el = document.createTextNode(token.value)
    frag.appendChild(el);
}

而在最后編譯完成,執(zhí)行linker時(shí)荡澎,主要做兩件事均践,第一是對(duì)需要雙向綁定的節(jié)點(diǎn)創(chuàng)建directive,第二是將整個(gè)文本節(jié)點(diǎn)進(jìn)行替換摩幔;怎么替換呢彤委?如最開(kāi)始是一個(gè)文本節(jié)點(diǎn)this is number: {{number}},經(jīng)過(guò)上面處理之后或衡,在frag中其實(shí)是兩個(gè)文本節(jié)點(diǎn)this is number:{{number}}焦影;此時(shí)就使用replaceChild方法使用新的節(jié)點(diǎn)替換原始的節(jié)點(diǎn);

// compile.js
function compileTextNode(el) {
    let tokens = parseText(el.wholeText);
    var frag = document.createDocumentFragment();
    for(let i = 0, _i = tokens.length; i < _i; i++) {
        let token = tokens[i], el = document.createTextNode(token.value)
        frag.appendChild(el);
    }

    return function(vm) {
        var fragClone = frag.cloneNode(true);
        var childNodes = Array.prototype.slice.call(fragClone.childNodes), token;

        // 創(chuàng)建directive
        ......

        // 通過(guò)這兒將`THIS IS TEST {{ number }} test` 這種轉(zhuǎn)化為三個(gè)textNode
        if(tokens.length) {
            replace(el, fragClone);
        }
    }   
}

// util.js
export function replace (target, el) {
  var parent = target.parentNode
  if (parent) {
    parent.replaceChild(el, target)
  }
}

替換后結(jié)果如下圖:

image

經(jīng)過(guò)與最開(kāi)始圖比較可以發(fā)現(xiàn)封断,已經(jīng)將this is number: {{number}} middle {{number2}}轉(zhuǎn)化為this is number: number middle number2斯辰;只是此時(shí),仍然展示的是變量名稱坡疼,如number,number2椒涯;那么,我們下面應(yīng)該做的肯定就是需要根據(jù)我們初始化時(shí)傳入的變量的值回梧,將其進(jìn)行正確的展示废岂;最終結(jié)果肯定應(yīng)該為this is number: 5 middle 2;即將number替換為5狱意、將number2替換為2;那么湖苞,如何實(shí)現(xiàn)上述功能呢,我們上面提過(guò)详囤,使用指令(directive)的方式财骨;下面镐作,就開(kāi)始進(jìn)行指令的處理;

Directive(指令)

對(duì)于每一個(gè)指令隆箩,肯定是隔離開(kāi)的该贾,互相不受影響且有自己的一套處理方式;所以捌臊,我們就使用對(duì)象的方式杨蛋;一個(gè)指令就是一個(gè)實(shí)例化的對(duì)象,彼此之間互不影響理澎;如下代碼:

export default function Directive(vm, el, description) {
    this.vm = vm;
    this.el = el;
    this.description = description;
    this.expression = description ? description.value : '';
}

在創(chuàng)建一個(gè)指令時(shí)逞力,需要傳入三個(gè)參數(shù),一個(gè)是最開(kāi)始初始化var vm = new vueImitate(options)時(shí)實(shí)例化的對(duì)象糠爬;而el是需要初始化指令的當(dāng)前元素寇荧,如<p v-show="showNode">this is test</p>,需要?jiǎng)?chuàng)建v-show的指令执隧,此時(shí)的el就是當(dāng)前的p標(biāo)簽揩抡;而description主要包含指令的描述信息;主要包含如下:

// 源碼見(jiàn) './directives/text.js'
var text = {
  bind () {
    this.attr = this.el.nodeType === 3
      ? 'data'
      : 'textContent'
  },
  update (value) {
    this.el[this.attr] = value
  }
}

// 如镀琉,'{{number}}'
description = {
    el: el, // 需要?jiǎng)?chuàng)建指令的元素
    def: text, // 對(duì)指令的操作方法捅膘,包括數(shù)據(jù)綁定(bind)、數(shù)據(jù)更新(update)滚粟,見(jiàn)上面 text
    name: 'text', // 指令名稱
    value: 'number' // 指令對(duì)應(yīng)數(shù)據(jù)的key
}

通過(guò)new Directive(vm, el, description)就創(chuàng)建了一個(gè)指令寻仗,并初始化一些數(shù)據(jù);下面就先通過(guò)指令對(duì)界面進(jìn)行數(shù)據(jù)渲染凡壤;所有邏輯就放到了_bind方法中署尤,如下:

// directive.js
Directive.prototype._bind = function() {
    extend(this, this.description.def);
    if(this.bind) {
        this.bind();
    }

    var self = this, watcher = new Watcher(this.vm, this.expression, function() {
        self.update(watcher.value);
    })

    if(this.update) {
        this.update(watcher.value);
    }
}

// util.js
export function extend(to, from) {
    Object.keys(from).forEach((key) => {
        to[key] = from[key];
    })
    return to;
}

方法首先將傳入的指令操作方法合并到this上,方便調(diào)用亚侠,主要包括上面說(shuō)的bind曹体、update等方法;其主要根據(jù)指令不同硝烂,功能不同而不同定義箕别;所有對(duì)應(yīng)均在./directives/*文件夾下面,包括文本渲染text.js滞谢、事件添加bind.js串稀、v-model對(duì)應(yīng)model.js、v-show對(duì)應(yīng)show.js等狮杨;通過(guò)合并以后母截,就執(zhí)行this.bind()方法進(jìn)行數(shù)據(jù)初始化綁定;但是橄教,目前為止清寇,當(dāng)去看界面時(shí)喘漏,仍然沒(méi)有將number轉(zhuǎn)化為5;為什么呢华烟?通過(guò)查看代碼:

export default {
  bind () {
    this.attr = this.el.nodeType === 3
      ? 'data'
      : 'textContent'
  },

  update (value) {
    this.el[this.attr] = value
  }
}

bind并沒(méi)有改變節(jié)點(diǎn)展示值翩迈,而是通過(guò)update; 所以,如果調(diào)用this.update(123)盔夜,可發(fā)現(xiàn)有如下結(jié)果:

image

其實(shí)我們并不是直接固定數(shù)值负饲,而是根據(jù)初始化時(shí)傳入的值動(dòng)態(tài)渲染;但是目前為止比吭,至少已經(jīng)完成了界面數(shù)據(jù)的渲染,只是數(shù)據(jù)不對(duì)而已姨涡;
然后衩藤,我們回頭看下編譯過(guò)程,我們需要在編譯過(guò)程去實(shí)例化指令(directive)涛漂,并調(diào)用其_bind方法赏表,對(duì)指令進(jìn)行初始化處理;

// 見(jiàn)compile.js 'function compileTextNode'
let _el = childNodes[j], description = {
    el: _el,
    name: 'text',
    value: tokens[j].value,
    def: publicDirectives['text']
}
vm._directives.push(new Directive(vm, _el, description));

// 見(jiàn)compile.js 'function compile'
let newDirectives = this._directives.slice(_dirLength);
for(let i = 0, _i = newDirectives.length; i < _i; i++) {
    newDirectives[i]._bind();
}

上面說(shuō)了匈仗,目前還沒(méi)有根據(jù)傳入的數(shù)據(jù)進(jìn)行綁定瓢剿,下面,就來(lái)對(duì)數(shù)據(jù)進(jìn)行處理悠轩;

數(shù)據(jù)處理

數(shù)據(jù)處理包括以下幾個(gè)方面:

  • 數(shù)據(jù)雙向綁定
  • 數(shù)據(jù)變化后间狂,需要通知到ui界面,并自動(dòng)變化
  • 對(duì)于輸入框火架,使用v-model時(shí)鉴象,需要將輸入內(nèi)容反應(yīng)到對(duì)應(yīng)數(shù)據(jù)

數(shù)據(jù)雙向綁定

需要實(shí)現(xiàn)雙向綁定,就是在數(shù)據(jù)變化后能夠自動(dòng)的將對(duì)應(yīng)界面進(jìn)行更新何鸡;那么纺弊,如何監(jiān)控?cái)?shù)據(jù)的變化呢?目前有幾種方式骡男,一種是angular的臟檢查方式淆游,就是對(duì)用戶所以操作、會(huì)導(dǎo)致數(shù)據(jù)變化的行為進(jìn)行攔截隔盛,如ng-click犹菱、$http$timeout等吮炕;當(dāng)用戶進(jìn)行請(qǐng)求數(shù)據(jù)已亥、點(diǎn)擊等時(shí),會(huì)對(duì)所有的數(shù)據(jù)進(jìn)行檢查来屠,如果數(shù)據(jù)變化了虑椎,就會(huì)觸發(fā)對(duì)應(yīng)的處理震鹉;而另一種是vue的實(shí)現(xiàn)方式,使用Object.definProperty()方法捆姜,對(duì)數(shù)據(jù)添加settergetter传趾;當(dāng)對(duì)數(shù)據(jù)進(jìn)行賦值時(shí),會(huì)自動(dòng)觸發(fā)setter泥技;就可以監(jiān)控?cái)?shù)據(jù)的變化浆兰;主要處理如下, 源碼見(jiàn)這里

export function Observer(data) {
    this.data = data;
    Object.keys(data).forEach((key) => {
        defineProperty(data, key, data[key]);
    })
}

export function observer(data, vm) {
    if(!data || typeof data !== 'object') {
        return;
    }

    let o = new Observer(data);
    return o;
}

function defineProperty(data, key, val) {
    let _value = data[key];
    let childObj = observer(_value);

    let dep = new Dep(); //生成一個(gè)調(diào)度中心,管理此字段的所有訂閱者
    Object.defineProperty(data, key, {
        enumerable: true, // 可枚舉
        configurable: false, // 不能再define
        get: function() {
            if (Dep.target) {
                dep.depend();
            }
            return val;
        },
        set: function(value) {
            val = value;
            childObj = observer(value);
            dep.notify();
        }
    })
}

Observer是一個(gè)構(gòu)造函數(shù)珊豹,主要對(duì)傳入的數(shù)據(jù)進(jìn)行Object.defineProperty綁定簸呈;可以監(jiān)控到數(shù)據(jù)的變化;而在每一個(gè)Observer中店茶,會(huì)初始化一個(gè)Dep的稱為‘調(diào)度管理器’的對(duì)象蜕便,它主要負(fù)責(zé)保存界面更新的操作和操作的觸發(fā);

界面更新

在通過(guò)上面Observer實(shí)現(xiàn)數(shù)據(jù)監(jiān)控之后贩幻,如何通知界面更新呢轿腺?這里使用了‘發(fā)布/訂閱模式’;如果需要對(duì)此模式進(jìn)行更深入理解丛楚,可查看此鏈接族壳;而每個(gè)數(shù)據(jù)key都會(huì)維護(hù)了一個(gè)獨(dú)立的調(diào)度中心Dep;通過(guò)在上面defineProperty時(shí)創(chuàng)建;而Dep主要保存數(shù)據(jù)更新后的處理任務(wù)及對(duì)任務(wù)的處理趣些,代碼也非常簡(jiǎn)單仿荆,就是使用subs保存所有任務(wù),使用addSub添加任務(wù)坏平,使用notify處理任務(wù)赖歌,depend作用會(huì)在下面watcher中進(jìn)行說(shuō)明:

// Dep.js

let uid = 0;
// 調(diào)度中心
export default function Dep() {
    this.id = uid++;
    this.subs = []; //訂閱者數(shù)組
    this.target = null; // 有何用處?
}

// 添加任務(wù)
Dep.prototype.addSub = function(sub) {
    this.subs.push(sub);
}

// 處理任務(wù)
Dep.prototype.notify = function() {
    this.subs.forEach((sub) => {
        if(sub && sub.update && typeof sub.update === 'function') {
            sub.update();
        }
    })
}

Dep.prototype.depend = function() {
    Dep.target.addDep(this);
}

那么功茴,處理任務(wù)來(lái)源哪兒呢庐冯?vue中又維護(hù)了一個(gè)watcher的對(duì)象,主要是對(duì)任務(wù)的初始化和收集處理坎穿;也就是一個(gè)watcher就是一個(gè)任務(wù)展父;而整個(gè)watcher代碼如下, 線上源碼見(jiàn)這里

export default function Watcher(vm, expression, cb) {
    this.cb = cb;
    this.vm = vm;
    this.expression = expression;
    this.depIds = {};

    if (typeof expression === 'function') {
        this.getter = expOrFn;
    } else {
        this.getter = this.parseGetter(expression);
    }

    this.value = this.get();
}

let _prototype = Watcher.prototype;

_prototype.update = function() {
    this.run();
}

_prototype.run = function() {
    let newValue = this.get(), oldValue = this.value;
    if(newValue != oldValue) {
        this.value = newValue;
        this.cb.call(this.vm, newValue);
    }
}

_prototype.addDep = function(dep) {
    // console.log(dep)
    if (!this.depIds.hasOwnProperty(dep.id)) {
        dep.addSub(this);
        this.depIds[dep.id] = dep;
    }
}

_prototype.get = function() {
    Dep.target = this;
    var value = this.getter && this.getter.call(this.vm, this.vm);
    Dep.target = null;
    return value;
}

_prototype.parseGetter = function(exp) {
    if (/[^\w.$]/.test(exp)) return; 

    var exps = exp.split('.');

    return function(obj) {
        let value = '';
        for (var i = 0, len = exps.length; i < len; i++) {
            if (!obj) return;
            value = obj[exps[i]];
        }
        return value;
    }
}

在初始化watcher時(shí),需要傳入vm(整個(gè)項(xiàng)目初始化時(shí)實(shí)例化的vueImitate對(duì)象玲昧,因?yàn)樾枰玫嚼锩娴膶?duì)應(yīng)數(shù)據(jù))栖茉、expression(任務(wù)對(duì)應(yīng)的數(shù)據(jù)的key,如上面的‘number’)孵延、cb(一個(gè)當(dāng)數(shù)據(jù)變化后吕漂,界面如何更新的函數(shù),也就是上面directive里面的update方法)尘应;我們需要實(shí)現(xiàn)功能有惶凝,第一是每個(gè)任務(wù)有個(gè)update方法吼虎,主要用于在數(shù)據(jù)變化時(shí),進(jìn)行調(diào)用苍鲜,即:

// 處理任務(wù)
Dep.prototype.notify = function() {
    this.subs.forEach((sub) => {
        if(sub && sub.update && typeof sub.update === 'function') {
            sub.update();
        }
    })
}

第二個(gè)是在初始化watcher時(shí)思灰,需要將實(shí)例化的watcher(任務(wù))放入調(diào)度中心depsubs中;如何實(shí)現(xiàn)呢混滔?這里洒疚,使用了一些黑科技,流程如下坯屿,這兒我們以expression為'number'為例:

1油湖、在初始化watcher時(shí),會(huì)去初始化一個(gè)獲取數(shù)據(jù)的方法this.getter就是领跛,能夠通過(guò)傳入的expression取出對(duì)應(yīng)的值乏德;如通過(guò)number取出對(duì)應(yīng)的初始化時(shí)的值5;

2、調(diào)用this.value = this.get();方法隔节,方法中會(huì)去數(shù)據(jù)源中取值鹅经,并將此時(shí)的watcher放入Dep.target中備用寂呛,并返回取到的值怎诫;

// watcher.js
_prototype.get = function() {
    Dep.target = this;
    var value = this.getter && this.getter.call(this.vm, this.vm);
    Dep.target = null;
    return value;
}

3、因?yàn)槲覀冊(cè)谏厦?code>Observer已經(jīng)對(duì)數(shù)據(jù)進(jìn)行了Object.defineProperty綁定贷痪,所以幻妓,當(dāng)上面2步取值時(shí),會(huì)觸發(fā)對(duì)應(yīng)的getter劫拢,如下, 觸發(fā)get函數(shù)之后肉津,因?yàn)樯厦?已經(jīng)初始化Dep.target = this;了,所以會(huì)執(zhí)行dep.depend();舱沧,就是上面說(shuō)的depend函數(shù)了:

// Observer.js
let dep = new Dep(); //生成一個(gè)調(diào)度中心妹沙,管理此字段的所有訂閱者
Object.defineProperty(data, key, {
    enumerable: true, // 可枚舉
    configurable: false, // 不能再define
    get: function() {
        if (Dep.target) {
            dep.depend();
        }
        return val;
    },
    set: function(value) {
        val = value;
        childObj = observer(value);
        dep.notify();
    }
})

3、觸發(fā)dep.depend();之后熟吏,如下代碼距糖,會(huì)執(zhí)行Dep.target.addDep(this);, 此時(shí)的this就是上面實(shí)例化的dep, Dep.target則對(duì)應(yīng)的是剛剛1步中實(shí)例化的watcher,即執(zhí)行watcher.addDep(dep);

// Dep.js
Dep.prototype.depend = function() {
    Dep.target.addDep(this);
}

4牵寺、觸發(fā)watcher.addDep(dep)悍引,如下代碼,如果目前還沒(méi)此dep帽氓;就執(zhí)行dep.addSub(this);,此時(shí)的this就是指代當(dāng)前watcher趣斤,也就是1步時(shí)實(shí)例化的watcher;此時(shí)dep是步驟3中實(shí)例化的dep; 即是黎休,dep.addSub(watcher);

// watcher.js
_prototype.addDep = function(dep) {
    // console.log(dep)
    if (!this.depIds.hasOwnProperty(dep.id)) {
        dep.addSub(this);
        this.depIds[dep.id] = dep;
    }
}

5浓领、最后執(zhí)行dep.addSub(watcher);玉凯,如下代碼,到這兒镊逝,就將初始化的watcher添加到了調(diào)度中心的數(shù)組中壮啊;

// Dep.js
Dep.prototype.addSub = function(sub) {
    this.subs.push(sub);
}

那么,在哪兒去初始化watcher呢撑蒜?就是在對(duì)指令進(jìn)行_bind()時(shí)歹啼,如下代碼,在執(zhí)行_bind時(shí)座菠,會(huì)實(shí)例化Watcher; 在第三個(gè)參數(shù)的回調(diào)函數(shù)里執(zhí)行self.update(watcher.value);狸眼,也就是當(dāng)監(jiān)控到數(shù)據(jù)變化,會(huì)執(zhí)行對(duì)應(yīng)的update方法進(jìn)行更新浴滴;

// directive.js
Directive.prototype._bind = function() {
    extend(this, this.description.def);
    if(this.bind) {
        this.bind();
    }
    var self = this, 
    watcher = new Watcher(this.vm, this.expression, function() {
        self.update(watcher.value);
    })
    if(this.update) {
        this.update(watcher.value);
    }
}

而前面說(shuō)了拓萌,開(kāi)始時(shí)沒(méi)有數(shù)據(jù),使用this.update(123)會(huì)將界面對(duì)應(yīng)number更新為123升略,當(dāng)時(shí)沒(méi)有對(duì)應(yīng)number真實(shí)數(shù)據(jù)微王;而此時(shí),在watcher中品嚣,獲取到了對(duì)應(yīng)數(shù)據(jù)并保存到value中炕倘,因此,就執(zhí)行this.update(watcher.value);翰撑,此時(shí)就可以將真實(shí)數(shù)據(jù)與界面進(jìn)行綁定罩旋,并且當(dāng)數(shù)據(jù)變化時(shí),界面也會(huì)自動(dòng)進(jìn)行更新眶诈;最終結(jié)果如下圖:

image

為什么所有數(shù)據(jù)都是undefined呢涨醋?我們可以通過(guò)下面代碼知道, 在實(shí)例化watcher時(shí),調(diào)用this.value = this.get();時(shí)逝撬,其實(shí)是通過(guò)傳入的key在this.vm中直接取值浴骂;但是我們初始化時(shí),所有值都是通過(guò)this.options = options || {};放到this.options里面宪潮,所以根本無(wú)法取到:

// watcher.js

_prototype.get = function() {
    Dep.target = this;
    var value = this.getter && this.getter.call(this.vm, this.vm);
    Dep.target = null;
    return value;
}
_prototype.parseGetter = function(exp) {
    if (/[^\w.$]/.test(exp)) return; 

    var exps = exp.split('.');

    return function(obj) {
        let value = '';
        for (var i = 0, len = exps.length; i < len; i++) {
            if (!obj) return;
            value = obj[exps[i]];
        }
        return value;
    }
}

那么溯警,我們?nèi)绾文苤苯涌梢酝ㄟ^(guò)諸如this.number取到值呢?只能如下坎炼,通過(guò)下面extend(this, data);方式愧膀,就將數(shù)據(jù)綁定到了實(shí)例化的vueImitate上面;

import { extend } from './util.js';
import { observer } from './Observer.js';
import Compile from './compile.js';

export default function vueImitate(options) {
    this.options = options || {};
    this.selector = options.el ? ('#' + options.el) : 'body';
    this.data = typeof options.data === 'function' ? options.data() : options.data;
    this.el = document.querySelectorAll(this.selector)[0];

    this._directives = [];

    this.initData();
    this.compile();
}

Compile(vueImitate);

vueImitate.prototype.initData = function() {
    let data = this.data, self = this;

    extend(this, data);

    observer(this.data);
}

處理后結(jié)果如下:

image

數(shù)據(jù)也綁定上了谣光,但是當(dāng)我們嘗試使用下面方式對(duì)數(shù)據(jù)進(jìn)行改變時(shí)檩淋,發(fā)現(xiàn)并沒(méi)有自動(dòng)更新到界面,界面數(shù)據(jù)并沒(méi)有變化;

methods: {
    add() {
        this.number1 += 1;
        this.number += 1;
    }
}

為什么呢蟀悦?通過(guò)上面代碼可知媚朦,我們其實(shí)observer的是vueImitate實(shí)例化對(duì)象的data對(duì)象;而我們更改值是通過(guò)this.number += 1;實(shí)現(xiàn)的日戈;其實(shí)并沒(méi)有改vueImitate.data.number的值询张,而是改vueImitate.number的值,所以也就不會(huì)觸發(fā)observer里面的setter浙炼;也不會(huì)去觸發(fā)對(duì)應(yīng)的watcher里面的update份氧;那如何處理呢?我們可以通過(guò)如下方式實(shí)現(xiàn), 完整源碼見(jiàn)這里

// init.js
vueImitate.prototype.initData = function() {
    let data = this.data, self = this;

    extend(this, data);

    Object.keys(data).forEach((key) => {
        Object.defineProperty(self, key, {
            set: function(newVal) {
                self.data[key] = newVal;
            },
            get: function() {
                return self.data[key];
            }
        })
    })

    observer(this.data);
}

這里通過(guò)對(duì)vueImitate里對(duì)應(yīng)的data的屬性進(jìn)行Object.defineProperty處理弯屈,當(dāng)對(duì)其進(jìn)行賦值時(shí)蜗帜,會(huì)再將其值賦值到vueImitate.data對(duì)應(yīng)的屬性上面,那樣资厉,就會(huì)去觸發(fā)observer(this.data);里面的setter厅缺,從而去更新界面數(shù)據(jù);

至此宴偿,整個(gè)數(shù)據(jù)處理就已經(jīng)完成湘捎,總結(jié)一下:

1、首先窄刘,在初始化vueImitate時(shí)窥妇,我們會(huì)將初始化數(shù)據(jù)通過(guò)options.data傳入,后會(huì)進(jìn)行處理都哭,保存至this.data中秩伞;

2逞带、通過(guò)initData方法將數(shù)據(jù)綁定到vueImitate實(shí)例化對(duì)象上面欺矫,并對(duì)其進(jìn)行數(shù)據(jù)監(jiān)控,然后使用observer對(duì)this.data進(jìn)行監(jiān)控展氓,在實(shí)例化Observer時(shí)穆趴,會(huì)去實(shí)例化一個(gè)對(duì)應(yīng)的調(diào)度中心Dep

3遇汞、在編譯過(guò)程中未妹,會(huì)創(chuàng)建指令寇蚊,通過(guò)指令實(shí)現(xiàn)每個(gè)需要處理節(jié)點(diǎn)的數(shù)據(jù)處理和雙向綁定讯赏;

4、在指令_bind()時(shí)茬腿,會(huì)去實(shí)例化對(duì)應(yīng)的watcher歪赢,創(chuàng)建一個(gè)任務(wù)化戳,主要實(shí)現(xiàn)數(shù)據(jù)獲取、數(shù)據(jù)變化時(shí)埋凯,對(duì)應(yīng)界面更新(也就是更新函數(shù)的調(diào)用)点楼、并將生成的watcher存儲(chǔ)到對(duì)應(yīng)的步驟2中實(shí)例化的調(diào)度中心中扫尖;

5、當(dāng)數(shù)據(jù)更新時(shí)掠廓,會(huì)觸發(fā)對(duì)應(yīng)的setter换怖,然后調(diào)用dep.notify();觸發(fā)調(diào)度中心中所有任務(wù)的更新,即執(zhí)行所有的watcher.update蟀瞧,從而實(shí)現(xiàn)對(duì)應(yīng)界面的更新沉颂;

到目前為止,整個(gè)框架的實(shí)現(xiàn)基本已經(jīng)完成悦污。其中包括compile兆览、linker、oberver塞关、directive(v-model抬探、v-show、v-bind帆赢、v-text)小压、watcher;如果需要更深入的研究椰于,可見(jiàn)項(xiàng)目代碼; 可以自己clone下來(lái)怠益,運(yùn)行起來(lái);文中有些可能思考不夠充分瘾婿,忘見(jiàn)諒蜻牢,也歡迎大家指正;

作者:payne_pf
鏈接:http://www.reibang.com/p/1f9e992eb509
來(lái)源:簡(jiǎn)書(shū)
簡(jiǎn)書(shū)著作權(quán)歸作者所有偏陪,任何形式的轉(zhuǎn)載都請(qǐng)聯(lián)系作者獲得授權(quán)并注明出處抢呆。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市笛谦,隨后出現(xiàn)的幾起案子抱虐,更是在濱河造成了極大的恐慌,老刑警劉巖饥脑,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恳邀,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡灶轰,警方通過(guò)查閱死者的電腦和手機(jī)谣沸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)笋颤,“玉大人乳附,你說(shuō)我怎么就攤上這事。” “怎么了许溅?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵瓤鼻,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我贤重,道長(zhǎng)茬祷,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任并蝗,我火速辦了婚禮祭犯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘滚停。我一直安慰自己沃粗,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布键畴。 她就那樣靜靜地躺著最盅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪起惕。 梳的紋絲不亂的頭發(fā)上涡贱,一...
    開(kāi)封第一講書(shū)人閱讀 51,462評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音惹想,去河邊找鬼问词。 笑死,一個(gè)胖子當(dāng)著我的面吹牛嘀粱,可吹牛的內(nèi)容都是我干的激挪。 我是一名探鬼主播,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼锋叨,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼垄分!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起悲柱,我...
    開(kāi)封第一講書(shū)人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤锋喜,失蹤者是張志新(化名)和其女友劉穎些己,沒(méi)想到半個(gè)月后豌鸡,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡段标,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年涯冠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片逼庞。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蛇更,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情派任,我是刑警寧澤砸逊,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站掌逛,受9級(jí)特大地震影響师逸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜豆混,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一篓像、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧皿伺,春花似錦、人聲如沸鸵鸥。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至淆攻,卻和暖如春瓶珊,著一層夾襖步出監(jiān)牢的瞬間唱较,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工召川, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留南缓,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓荧呐,卻偏偏與公主長(zhǎng)得像汉形,于是被迫代替她去往敵國(guó)和親纸镊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354