新聞中心
Vue2.x響應(yīng)式原理怎么實(shí)現(xiàn)的?
Vue 最獨(dú)特的特性之一,是其非侵入性的響應(yīng)式系統(tǒng)。那么什么是響應(yīng)式原理?

讓客戶滿意是我們工作的目標(biāo),不斷超越客戶的期望值來(lái)自于我們對(duì)這個(gè)行業(yè)的熱愛(ài)。我們立志把好的技術(shù)通過(guò)有效、簡(jiǎn)單的方式提供給客戶,將通過(guò)不懈努力成為客戶在信息化領(lǐng)域值得信任、有價(jià)值的長(zhǎng)期合作伙伴,公司提供的服務(wù)項(xiàng)目有:域名申請(qǐng)、網(wǎng)絡(luò)空間、營(yíng)銷軟件、網(wǎng)站建設(shè)、城關(guān)網(wǎng)站維護(hù)、網(wǎng)站推廣。
數(shù)據(jù)模型僅僅是普通的JavaScript對(duì)象,而當(dāng)我們修改數(shù)據(jù)時(shí),視圖會(huì)進(jìn)行更新,避免了繁瑣的DOM操作,提高開(kāi)發(fā)效率。簡(jiǎn)言之,在改變數(shù)據(jù)的時(shí)候,視圖會(huì)跟著更新。
了解概念之后,那么它是怎么實(shí)現(xiàn)的呢?
其實(shí)是利用Object.defineProperty()中的getter 和setter方法和設(shè)計(jì)模式中的觀察者模式。
那么,我們先來(lái)看下Object.defineProperty()。MDN中它是這樣解釋它的:Object.defineProperty()方法會(huì)直接在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)對(duì)象的現(xiàn)有屬性,并返回此對(duì)象。
- let data = {
- msg:'hello'
- };
- let vm = {};
- Object.defineProperty(vm, 'msg', {
- enumerable: true, // 可枚舉(可遍歷)
- configurable: true, // 可配置(可以使用delete 刪除,可以通過(guò)defineProperty重新定義)
- // 當(dāng)獲取值的時(shí)候執(zhí)行
- get() {
- console.log('get', data.msg);
- return data.msg
- },
- // 當(dāng)設(shè)置值的時(shí)候執(zhí)行
- set(newVal) {
- if (newVal === data.msg) {
- return
- }
- data.msg = newVal;
- console.log('set', data.msg);
- }
- })
- // 測(cè)試
- console.log(vm.msg);
- /*
- > "get" "hello"
- > "hello"
- */
- vm.msg = 'world'; // > "set" "world"
簡(jiǎn)單介紹Object.defineProperty()之后,接著就是了解觀察者模式,看到它,你可能會(huì)想起發(fā)布-訂閱模式。其實(shí)它們的本質(zhì)是相同的,但是也存在一定的區(qū)別。
我們不妨先來(lái)看下發(fā)布-訂閱模式。
發(fā)布-訂閱者模式里面包含了三個(gè)模塊,發(fā)布者,訂閱者和統(tǒng)一調(diào)度中心。這里統(tǒng)一調(diào)度中心相當(dāng)于報(bào)刊辦事大廳。發(fā)布者相當(dāng)與某個(gè)雜志負(fù)責(zé)人,他來(lái)中心這注冊(cè)一個(gè)的雜志,而訂閱者相當(dāng)于用戶,我在中心訂閱了這分雜志。每當(dāng)發(fā)布者發(fā)布了一期雜志,辦事大廳就會(huì)通知訂閱者來(lái)拿新雜志。發(fā)布-訂閱者模式由統(tǒng)一調(diào)度中心調(diào)用,因此發(fā)布者和訂閱者不需要知道對(duì)方的存在。
下面,我們將通過(guò)一個(gè)實(shí)現(xiàn)Vue自定義事件的例子來(lái)更進(jìn)一步了解發(fā)布-訂閱模式。
- function EventEmitter(){
- // 初始化統(tǒng)一調(diào)度中心
- this.subs = Object.create(null); // {'click':[fn1,fn2]}
- }
- // 注冊(cè)事件
- EventEmitter.prototype.$on = function (eventType,handler){
- console.log(this);
- this.subs[eventType]= this.subs[eventType]||[];
- this.subs[eventType].push(handler);
- }
- // 觸發(fā)事件
- EventEmitter.prototype.$emit = function (eventType,data){
- if(this.subs[eventType]){
- this.subs[eventType].forEach(handler => {
- handler(data);
- });
- }
- }
- // 測(cè)試
- const em = new EventEmitter();
- //訂閱者
- em.$on('click1',(data)=>{
- console.log(data);
- })
- // 發(fā)布者
- em.$emit('click1','maomin') //maomin
這種自定義事件廣泛應(yīng)用于Vue同級(jí)組件傳值。
接下來(lái),我們來(lái)介紹觀察者模式。
觀察者模式是由目標(biāo)調(diào)度,比如當(dāng)事件觸發(fā)時(shí),目標(biāo)就會(huì)調(diào)用觀察者的方法,所以觀察者模式的訂閱者(觀察者)與發(fā)布者(目標(biāo))之間存在依賴。
- // 發(fā)布者(目標(biāo))
- function Dep(){
- this.subs = [];
- }
- Dep.prototype.addSub = function (sub){
- if(sub&&sub.update){
- this.subs.push(sub);
- }
- }
- Dep.prototype.notify = function (data){
- this.subs.forEach(sub=>{
- sub.update(data);
- })
- }
- // 訂閱者(觀察者)
- function Watcher(){}
- Watcher.prototype.update=function(data){
- console.log(data);
- }
- // 測(cè)試
- let dep = new Dep();
- let watcher = new Watcher();
- // 收集依賴
- dep.addSub(watcher);
- // 發(fā)送通知
- dep.notify('1');
- dep.notify('2');
下圖是區(qū)分兩種模式。
實(shí)現(xiàn)Vue2.x迷你版本
為什么要實(shí)現(xiàn)一個(gè)Vue迷你版本,目的就是加深對(duì)Vue響應(yīng)式原理以及其中一些API的理解。首先我們先來(lái)分析Vue2.x 響應(yīng)式原理的整體結(jié)構(gòu)。
如下圖所示:
我們接下來(lái),將根據(jù)這幅圖片描述的流程來(lái)實(shí)現(xiàn)一款迷你版Vue。Vue2.x采用了Virtual DOM,但是因?yàn)檫@里只需要實(shí)現(xiàn)一個(gè)迷你版,所以我們這里做了簡(jiǎn)化,我們這里就是直接操作DOM。
下面,我們來(lái)看下我是如何搭建一款Vue mini的。
第一步
頁(yè)面結(jié)構(gòu)如下,我們可以先引入Vue2.x完整版本,看下實(shí)現(xiàn)效果。
Vue2.x Reactive 文本節(jié)點(diǎn)
{{msg}} {{count}} {{obj.name}} {{arr[0]}} {{obj.inner.age}} {{obj.inner.name}}v-text
v-model
v-html
v-show
{{isShow}}v-on
v-if
{{isIf}}
- const vm = new Vue({
- el: '#app',
- data() {
- return {
- msg: 'maomin',
- count: 1,
- obj: {
- name: 'hello',
- inner: {
- age: 17
- }
- },
- arr: ['string1'],
- html: '
{{msg}}',- isShow: false,
- isIf:true
- }
- },
- methods: {
- handler() {
- // this.count = 2;
- this.isIf = !this.isIf;
- },
- onClick() {
- this.obj.inner.age = 18;
- // console.log(this.obj.inner.age);
- }
- }
- });
經(jīng)過(guò)測(cè)試,Vue2.x完整版搭載的頁(yè)面顯示如下。我們將使用Vue迷你版本同樣實(shí)現(xiàn)以下頁(yè)面效果。
第二步
我們將根據(jù)整體結(jié)構(gòu)圖和頁(yè)面結(jié)構(gòu)來(lái)搭建這個(gè)Vue迷你版本,我們姑且將這個(gè)版本叫做vuemini.js。
通過(guò)整體結(jié)構(gòu)圖我們發(fā)現(xiàn),一共有Vue、Observer、Compiler、Dep、Watcher這幾個(gè)構(gòu)造函數(shù)。我們首先創(chuàng)建這幾個(gè)構(gòu)造函數(shù),這里不使用class類來(lái)定義是因?yàn)閂ue源碼大部分也使用構(gòu)造函數(shù),另外,相對(duì)也好拓展。
Vue
- // 實(shí)例。
- function Vue(options) {
- this.$options = options || {};
- this._data = typeof options.data === 'function' ? options.data() : options.data || {};
- this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
- // 負(fù)責(zé)把data中的屬性注入到Vue實(shí)例,轉(zhuǎn)換成getter/setter
- this._proxyData(this._data);
- this.initMethods(this, options.methods || {})
- // 負(fù)責(zé)調(diào)用observer監(jiān)聽(tīng)data中所有屬性的變化
- new Observer(this._data);
- // 負(fù)責(zé)調(diào)用compiler解析指令/插值表達(dá)式
- new Compiler(this);
- }
- // 將data中的屬性掛載到this上
- Vue.prototype._proxyData = function (data) {
- Object.keys(data).forEach(key => {
- Object.defineProperty(this, key, {
- configurable: true,
- enumerable: true,
- get() {
- return data[key]
- },
- set(newVal) {
- if (newVal === data[key]) {
- return
- }
- data[key] = newVal;
- }
- })
- })
- }
- function noop(a, b, c) { }
- function polyfillBind(fn, ctx) {
- function boundFn(a) {
- var l = arguments.length;
- return l
- ? l > 1
- ? fn.apply(ctx, arguments)
- : fn.call(ctx, a)
- : fn.call(ctx)
- }
- boundFn._length = fn.length;
- return boundFn
- }
- function nativeBind(fn, ctx) {
- return fn.bind(ctx)
- }
- const bind = Function.prototype.bind
- ? nativeBind
- : polyfillBind;
- // 初始化methods屬性
- Vue.prototype.initMethods = function (vm, methods) {
- for (var key in methods) {
- {
- if (typeof methods[key] !== 'function') {
- warn(
- "Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +
- "Did you reference the function correctly?",
- vm
- );
- }
- }
- vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
- }
- }
Observer
- // 數(shù)據(jù)劫持。
- // 負(fù)責(zé)把data(_data)選項(xiàng)中的屬性轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)。
- function Observer(data) {
- this.walk(data);
- }
- Observer.prototype.walk = function (data) {
- if (!data || typeof data !== 'object') {
- return
- }
- Object.keys(data).forEach(key => {
- this.defineReactive(data, key, data[key]);
- })
- }
- Observer.prototype.defineReactive = function (obj, key, val) {
- let that = this;
- // 負(fù)責(zé)收集依賴
- let dep = new Dep();
- // 如果val是對(duì)象,把val內(nèi)部的屬性轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)
- this.walk(val);
- Object.defineProperty(obj, key, {
- enumerable: true,
- configurable: true,
- get() {
- // 收集依賴
- Dep.target && dep.addSub(Dep.target)
- return val
- },
- set(newVal) {
- if (newVal === val) {
- return
- }
- val = newVal;
- // data內(nèi)屬性重新賦值后,使其轉(zhuǎn)化為響應(yīng)式數(shù)據(jù)。
- that.walk(newVal);
- // 發(fā)送通知
- dep.notify();
- }
- })
- }
Compiler
- // 編譯模板,解析指令/插值表達(dá)式
- // 負(fù)責(zé)頁(yè)面的首次渲染
- // 當(dāng)數(shù)據(jù)變化時(shí)重新渲染視圖
- function Compiler(vm) {
- this.el = vm.$el;
- this.vm = vm;
- // 立即編譯模板
- this.compile(this.el);
- }
- // 編譯模板,處理文本節(jié)點(diǎn)和元素節(jié)點(diǎn)
- Compiler.prototype.compile = function (el) {
- let childNodes = el.childNodes;
- Array.from(childNodes).forEach(node => {
- // 處理文本節(jié)點(diǎn)
- if (this.isTextNode(node)) {
- this.compileText(node);
- }
- // 處理元素節(jié)點(diǎn)
- else if (this.isElementNode(node)) {
- this.compileElement(node);
- }
- // 判斷node節(jié)點(diǎn),是否有子節(jié)點(diǎn),如果有子節(jié)點(diǎn),要遞歸調(diào)用compile方法
- if (node.childNodes && node.childNodes.length) {
- this.compile(node);
- }
- })
- }
- // 編譯文本節(jié)點(diǎn),處理插值表達(dá)式
- Compiler.prototype.compileText = function (node) {
- // console.dir(node);
- let reg = /\{\{(.+?)\}\}/;
- let value = node.textContent;
- if (reg.test(value)) {
- let key = RegExp.$1.trim();
- if (this.vm.hasOwnProperty(key)) {
- node.textContent = value.replace(reg, typeof this.vm[key] === 'object' ? JSON.stringify(this.vm[key]) : this.vm[key]);
- // 創(chuàng)建watcher對(duì)象,當(dāng)數(shù)據(jù)改變更新視圖
- new Watcher(this.vm, key, (newVal) => {
- node.textContent = newVal;
- })
- } else {
- const str = `this.vm.${key}`;
- node.textContent = value.replace(reg, eval(str));
- // 創(chuàng)建watcher對(duì)象,當(dāng)數(shù)據(jù)改變更新視圖
- new Watcher(this.vm, key, () => {
- const strw = `this.vm.${key}`;
- node.textContent = value.replace(reg, eval(strw));
- })
- }
- }
- }
- // 判斷節(jié)點(diǎn)是否是文本節(jié)點(diǎn)
- Compiler.prototype.isTextNode = function (node) {
- return node.nodeType === 3;
- }
- // 判斷節(jié)點(diǎn)是否是元素節(jié)點(diǎn)
- Compiler.prototype.isElementNode = function (node) {
- return node.nodeType === 1;
- }
- // 編譯元素節(jié)點(diǎn),處理指令
- Compiler.prototype.compileElement = function (node) {
- // console.log(node.attributes);
- // 遍歷所有的屬性節(jié)點(diǎn)
- Array.from(node.attributes).forEach(attr => {
- let attrName = attr.name;
- // console.log(attrName);
- // 判斷是否是指令
- if (this.isDirective(attrName)) {
- // 判斷:如v-on:click
- let eventName;
- if (attrName.indexOf(':') !== -1) {
- const strArr = attrName.substr(2).split(':');
- attrName = strArr[0];
- eventName = strArr[1];
- } else if (attrName.indexOf('@') !== -1) {
- eventName = attrName.substr(1);
- attrName = 'on';
- } else {
- attrName = attrName.substr(2);
- }
- let key = attr.value;
- this.update(node, key, attrName, eventName);
- }
- })
- }
- // 判斷元素屬性是否是指令
- Compiler.prototype.isDirective = function (attrName) {
- return attrName.startsWith('v-') || attrName.startsWith('@');
- }
- // 指令輔助函數(shù)
- Compiler.prototype.update = function (node, key, attrName, eventName) {
- let updateFn = this[attrName + 'Updater'];
- updateFn && updateFn.call(this, node, this.vm[key], key, eventName);
- }
- // 處理v-text指令
- Compiler.prototype.textUpdater = function (node, value, key) {
- node.textContent = value;
- new Watcher(this.vm, key, (newVal) => {
- node.textContent = newVal;
- })
- }
- // 處理v-html指令
- Compiler.prototype.htmlUpdater = function (node, value, key) {
- node.insertAdjacentHTML('beforeend', value);
- new Watcher(this.vm, key, (newVal) => {
- node.insertAdjacentHTML('beforeend', newVal);
- })
- }
- // 處理v-show指令
- Compiler.prototype.showUpdater = function (node, value, key) {
- !value ? node.style.display = 'none' : node.style.display = 'block'
- new Watcher(this.vm, key, (newVal) => {
- !newVal ? node.style.display = 'none' : node.style.display = 'block';
- })
- }
- // 處理v-if指令
- Compiler.prototype.ifUpdater = function (node, value, key) {
- const nodew = node;
- const nodep = node.parentNode;
- if (!value) {
- node.parentNode.removeChild(node)
- }
- new Watcher(this.vm, key, (newVal) => {
- console.log(newVal);
- !newVal ? nodep.removeChild(node) : nodep.appendChild(nodew);
- })
- }
- // 處理v-on指令
- Compiler.prototype.onUpdater = function (node, value, key, eventName) {
- if (eventName) {
- const handler = this.vm.$options.methods[key].bind(this.vm);
- node.addEventListener(eventName, handler);
- }
- }
- // 處理v-model指令
- Compiler.prototype.modelUpdater = function (node, value, key) {
- node.value = value;
- new Watcher(this.vm, key, (newVal) => {
- node.value = newVal;
- })
- // 雙向綁定,視圖變化更新數(shù)據(jù)
- node.addEventListener('input', () => {
- this.vm[key] = node.value;
- })
- }
Dep
- // 發(fā)布者。
- // 收集依賴,添加所有的觀察者(watcher)。通知所有的觀察者。
- function Dep() {
- // 存儲(chǔ)所有的觀察者watcher
- this.subs = [];
- }
- // 添加觀察者
- Dep.prototype.addSub = function (sub) {
- if (sub && sub.update) {
- this.subs.push(sub);
- }
- }
- // 發(fā)送通知
- Dep.prototype.notify = function () {
- this.subs.forEach(sub => {
- sub.update();
- })
- }
Watcher
- function Watcher(vm, key, cb) {
- this.vm = vm;
- this.key = key;
- this.cb = cb;
- // 把當(dāng)前watcher對(duì)象記錄到Dep類的靜態(tài)屬性target
- Dep.target = this;
- if (vm.hasOwnProperty(key)) {
- this.oldVal = vm[key];
- } else {
- const str = `vm.${key}`;
- this.oldVal = eval(str);
- }
- Dep.target = null;
- }
- // 當(dāng)數(shù)據(jù)發(fā)生變化的時(shí)候更新視圖
- Watcher.prototype.update = function () {
- let newVal;
- if (this.vm.hasOwnProperty(this.key)) {
- newVal = this.vm[this.key];
- } else {
- const str = `this.vm.${this.key}`;
- newVal = eval(str);
- }
- this.cb(newVal);
- }
以上這幾個(gè)構(gòu)造函數(shù)就實(shí)現(xiàn)了我們所說(shuō)的迷你版本,將它們整合成一個(gè)文件vuemini.js。在上面所提示的頁(yè)面引入,查看效果。
另外,我在data中綁定了一個(gè)html屬性,值為一個(gè)'
尤大開(kāi)發(fā)的Vue2.x迷你版本
下面,我們將看下尤大開(kāi)發(fā)的迷你版本,這個(gè)版本引入了Virtual DOM,但是主要是針對(duì)響應(yīng)式式原理的,可以根據(jù)尤大的迷你版本與上面的版本作個(gè)比較,可以看下有哪些相似之處。
vue2mini - // reactivity ---
- let activeEffect
- class Dep {
- subscribers = new Set()
- depend() {
- if (activeEffect) {
- this.subscribers.add(activeEffect)
- }
- }
- notify() {
- this.subscribers.forEach(effect => effect())
- }
- }
- function watchEffect(effect) {
- activeEffect = effect
- effect()
- activeEffect = null
- }
- function reactive(raw) {
- // use Object.defineProperty
- // 1. iterate over the existing keys
- Object.keys(raw).forEach(key => {
- // 2. for each key: create a corresponding dep
- const dep = new Dep()
- // 3. rewrite the property into getter/setter
- let realValue = raw[key]
- Object.defineProperty(raw, key, {
- &nbs
當(dāng)前名稱:淺析Vue響應(yīng)系統(tǒng)原理與搭建Vue2.x迷你版
當(dāng)前地址:http://fisionsoft.com.cn/article/dppedjc.html


咨詢
建站咨詢
