vue2的雙向數(shù)據(jù)綁定(又稱響應(yīng)式)原理,是通過數(shù)據(jù)劫持結(jié)合發(fā)布訂閱模式的方式來實(shí)現(xiàn)的,通過Object.defineProperty()
來劫持各個(gè)屬性的setter
,getter
,在數(shù)據(jù)變動(dòng)時(shí)發(fā)布消息給訂閱者,觸發(fā)相應(yīng)的監(jiān)聽回調(diào)來渲染視圖。也就是說數(shù)據(jù)和視圖同步,數(shù)據(jù)發(fā)生變化,視圖跟著變化,視圖變化,數(shù)據(jù)也隨之發(fā)生改變。
Object.defineProperty
- 第一個(gè)參數(shù) object對(duì)象
- 第二個(gè)參數(shù) 屬性名
- 第三個(gè)參數(shù) 屬性描述符 這里只介紹
get
和set
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
get() {
console.log('此處進(jìn)行依賴的收集')
return value
},
set(newValue) {
if(newValue === value) return;
console.log('此處通知變化,執(zhí)行更新操作')
value = newValue;
}
})
}
const data = {
message: '1',
msg:'2'
}
let keys = Object.keys(data);
for(let i = 0; i< keys.length; i++) {
let key = keys[i];
let value = data[key];
// 把data中的每一項(xiàng)變成響應(yīng)式數(shù)據(jù)
defineReactive(data,key, value)
}
console.log(data.msg);
data.message = 'nihao'
從這個(gè)例子我們可以簡單理解一下數(shù)據(jù)劫持,當(dāng)我們用到了某個(gè)數(shù)據(jù)(比如在頁面上、在計(jì)算屬性中、在watch中),那么肯定會(huì)對(duì)這個(gè)數(shù)據(jù)進(jìn)行訪問,所以只要在getter中對(duì)數(shù)據(jù)的使用進(jìn)行攔截,對(duì)這個(gè)數(shù)據(jù)的使用(也就是此數(shù)據(jù)的依賴)進(jìn)行收集,收集好之后方便我們進(jìn)行后續(xù)的處理。當(dāng)我們改變某個(gè)數(shù)據(jù)的時(shí)候,需要用setter進(jìn)行攔截,對(duì)我們?cè)趃etter里面收集到的依賴進(jìn)行響處理,來改變視圖。
雙向數(shù)據(jù)綁定及模版編譯
vue初始化
- 初始化數(shù)據(jù)
- 初始化data
- 初始化watch
- 初始化computed
- 掛載
let vm = new Vue({
el: '#app',
data() {
return {
msg:'1',
message: 'hello',
obj: {
a:1,
b:2
}
}
},
watch: {
message: function (newValue, oldValue) {
console.log(newValue, oldValue)
}
},
computed: {
mm: function () {
return this.message + this.msg;
}
},
})
Vue.prototype._init = function(options) {
// 保存實(shí)例
let vm = this;
vm.$options = options;
initState(vm);
if(vm.$options.el) {
vm.$mount();
}
}
渲染W(wǎng)atcher(RenderWatcher)依賴的收集
初始化data
- 觀察數(shù)據(jù),把data下對(duì)象都變成響應(yīng)式數(shù)據(jù)
- 每一個(gè)響應(yīng)式數(shù)據(jù),都有一個(gè)對(duì)應(yīng)的實(shí)例化dep,作用是對(duì)依賴進(jìn)行收集以及數(shù)據(jù)改變更新視圖
Observer
export function defineReactive(data, key, value) {
let childOb = observe(value) // 遞歸把對(duì)象變成響應(yīng)式
let dep = new Dep(key);
// 不兼容IE8及以下
Object.defineProperty(data, key, {
get() {
if(Dep.target) {
dep.depend();
if(childOb) {
childOb.dep.depend(); // 數(shù)組的依賴收集
dependArray(value)
}
}
return value;
},
set(newValue) {
if(newValue === value) return
observe(newValue)
value = newValue
dep.notify()
}
});
}
class Observer {
constructor(data) {
this.dep = new Dep();
// 為了讓數(shù)組上有dep
Object.defineProperty(data, '__ob__', {
get: () => this
})
if(Array.isArray(data)) {
// 數(shù)組單獨(dú)處理
data.__proto__ = newArrayProtoMethods;
observeArray(data); // 對(duì)數(shù)組本身存在的對(duì)象進(jìn)行觀察
} else {
this.walk(data)
}
}
walk(data) {
// 拿到key值
let keys = Object.keys(data)
for(let i = 0; i< keys.length; i++) {
let key = keys[i]
let value = data[key]
// 變成響應(yīng)式數(shù)據(jù)
defineReactive(data, key, value)
}
}
}
Dep
- 對(duì)響應(yīng)數(shù)據(jù)的依賴進(jìn)行收集
- 數(shù)據(jù)改變時(shí),對(duì)依賴進(jìn)行update處理
-
depend
函數(shù)使dep
和watcher
相互依賴
let id = 0;
class Dep {
constructor() {
this.id = id++;
this.subs = [];
}
// 發(fā)布訂閱模式
// 訂閱
addSub(watcher) {
this.subs.push(watcher)
}
// 發(fā)布
notify() {
this.subs.forEach(watcher => watcher.update());
}
// 會(huì)把watcher存到dep當(dāng)中,還會(huì)把dep存到
depend() {
if(Dep.target) {
Dep.target.addDep(this);
}
}
}
let stack = [];
// watcher進(jìn)棧
export function pushTarget(watcher) {
Dep.target = watcher;
stack.push(watcher)
}
// watcher出棧
export function popTarget() {
stack.pop()
Dep.target = stack[stack.length - 1];
}
以在頁面上渲染message
為例
import Vue from 'vue';
let vm = new Vue({
el: '#app',
data() {
return {
message: 'hello',
}
},
})
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
{{ message }}
</div>
</body>
</html>
$mount
- 實(shí)例化一個(gè)RenderWatcher,傳入當(dāng)前的實(shí)例和頁面渲染函數(shù)
-
_update
里面涉及到模版編譯,我們可以先忽略模版編譯的內(nèi)容,只需要知道模版編譯的時(shí)候要取實(shí)例上的對(duì)象比如vm.message
Vue.prototype.$mount = function() {
let vm = this;
let el = vm.$options.el;
el = vm.$el = query(el);
// 首次渲染組件或者因?yàn)閿?shù)據(jù)變化重新渲染組件
let updateComponent = () => {
vm._update();
}
// 渲染watcher
new Watcher(vm, updateComponent);
}
Vue.prototype._update = function() {
let vm = this;
let el = vm.$el;
// 不直接操作dom,把dom先保存到文件碎片里面,文件碎片是保存在內(nèi)存中的,把操作結(jié)果一次塞到dom中去
let node = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild) {
node.appendChild(firstChild);
}
// 編譯 處理{{ message }} -> vm.message -> 觸發(fā)message的getter -> 進(jìn)行依賴的收集
compiler(node, vm);
el.appendChild(node)
}
Watcher
class Watcher {
constructor(vm , exprOrFn, cb = () => {}, opts = {}) {
this.vm = vm;
this.exprOrFn = exprOrFn;
this.getter = exprOrFn;
this.depsId = new Set()
this.deps = []
this.id = id++; // 每次實(shí)例化watcher的時(shí)候id++,可以通過不同的id區(qū)分不同的watcher
this.get();
}
get() {
pushTarget(this); // Dep.target = 當(dāng)前的watcher
this.getter();
popTarget();
return value;
}
update() {
this.get();
}
depend() {
// 作用把watcher里面的addDep執(zhí)行
let i = this.deps.length;
while(i--) {
this.deps[i].depend();
}
}
// watcher和dep互相依賴
addDep(dep) {
let id = dep.id;
if(!this.depsId.has(id)) {
this.depsId.add(id);
this.deps.push(dep); // 把dep存在了watcher中
dep.addSub(this) // 把watcher存在dep中
}
}
}
執(zhí)行g(shù)etter的時(shí)候,先把當(dāng)前的
RenderWatcher
push到stack
中,并且把Dep.target
設(shè)置為當(dāng)前的renderWatcher
。其次時(shí)執(zhí)行渲染函數(shù),取實(shí)例上的值vm.message
,這時(shí)候會(huì)走到message
的setter
,執(zhí)行dep.depend()
操作,depend
會(huì)把當(dāng)前的dep
存在RenderWatcher
中,也會(huì)把當(dāng)前的renderWatcher
存在dep
中。執(zhí)行完后把RenderWatcher
pop掉,并且把Dep.target
指向前一個(gè)依賴。
改變數(shù)據(jù)
let vm = new Vue({
el: '#app',
data() {
return {
message: 'hello',
}
},
})
setTimeout(() =>{
vm.message = 'hi'
}, 3000);
set(newValue) {
if(newValue === value) return
observe(newValue)
value = newValue
dep.notify()
}
notify() {
this.subs.forEach(watcher => watcher.update());
}
改變響應(yīng)式數(shù)據(jù)的時(shí)候,會(huì)在
setter
里面進(jìn)行攔截,執(zhí)行dep.notify
,他會(huì)循環(huán)執(zhí)行dep.subs
里面保存watcher
(即依賴)的update
函數(shù),這里等同于get
函數(shù),又會(huì)執(zhí)行同上的操作,數(shù)據(jù)的改變會(huì)導(dǎo)致視圖的變化。
用戶Watcher(UserWatcher)依賴的收集
初始化watch
let vm = new Vue({
el: '#app',
data() {
return {
message: 'hello',
}
},
watch: {
message: function (newValue, oldValue) {
console.log(newValue, oldValue)
}
},
})
setTimeout(() =>{
vm.message = 'hi'
}, 3000);
function createWatcher(vm, key, handler) {
return vm.$watch(key, handler)
}
function initWatch(vm) {
let watch = vm.$options.watch; // 拿到實(shí)例上的watcher
for(let key in watch) {
let handler = watch[key]
createWatcher(vm, key, handler)
}
}
Vue.prototype.$watch = function (expr, handler) {
let vm = this;
new Watcher(vm, expr, handler, {user: true}); // user: true 區(qū)分于渲染watcher
}
初始化
watch
的時(shí)候,會(huì)遍歷每一個(gè)屬性,每一個(gè)屬性都有一個(gè)實(shí)例化的watcher
,這種watcher
叫做UserWatcher
。
class Watcher {
constructor(vm , exprOrFn, cb = () => {}, opts = {}) {
this.vm = vm;
this.exprOrFn = exprOrFn;
this.getter = function () {
return getValue(exprOrFn, vm) // vm.message
}
if(opts.user) {
this.user = true;
}
this.cb = cb;
this.opts = opts;
this.depsId = new Set()
this.deps = []
this.id = id++; // 每次實(shí)例化watcher的時(shí)候id++,可以通過不同的id區(qū)分不同的watcher
this.value = this.get(); // 老的oldValue
}
get() {
pushTarget(this); // Dep.target = 當(dāng)前的watcher
let value = this.getter.call(this.vm);
if(this.value !== value) {
this.cb(value, this.value)
}
popTarget();
return value;
}
update() {
this.get();
}
run() {
let value = this.get(); // 新的newValue
if(this.value !== value) {
this.cb(value, this.value)
}
}
depend() {
// 作用把watcher里面的addDep執(zhí)行
let i = this.deps.length;
while(i--) {
this.deps[i].depend();
}
}
// watcher和dep互相依賴
addDep(dep) {
let id = dep.id;
if(!this.depsId.has(id)) {
this.depsId.add(id);
this.deps.push(dep); // 把dep存在了watcher中
dep.addSub(this) // 把watcher存在dep中
}
}
}
實(shí)例化UserWatcher的時(shí)候,執(zhí)行
get
,此時(shí)Dep.target
指向UserWatcher
,并且stack
中push這個(gè)UserWatcher
,getValue
的時(shí)候會(huì)取值vm.message
,所以會(huì)執(zhí)行dep.depend
進(jìn)行依賴的收集,此時(shí)message的dep中就存在這個(gè)userWatcher
了。之后把UserWatcher
從stack
中pop掉。
當(dāng)3s鐘后數(shù)據(jù)改變的時(shí)候,setter攔截,dep.notify(),將messge的dep中subs遍歷執(zhí)行依賴的update,又要執(zhí)行pushTarget、getter、popTareget等操作
計(jì)算屬性Watcher(ComputedWatcher)依賴的收集
初始化computed
let vm = new Vue({
el: '#app',
data() {
return {
msg:'1',
message: 'hello',
}
},
computed: {
mm: function () {
return this.message + this.msg;
}
},
})
setTimeout(() =>{
vm.message = 'hi'
}, 3000);
function createComputedGetter(vm, key) {
let watcher = vm._watchersComputed[key]; // 當(dāng)前的計(jì)算屬性watcher
return function () {
if(watcher) {
if(watcher.dirty) {
watcher.evaluate()
}
if(Dep.target) {
watcher.depend();
}
return watcher.value;
}
}
}
function initComputed(vm, computed) {
let watchers = vm._watchersComputed = Object.create(null); // 創(chuàng)建空對(duì)象,為了在實(shí)例里面更加方便的訪問到計(jì)算屬性的watcher
for (let key in computed) {
let userDef = computed[key];
watchers[key] = new Watcher(vm, userDef, () => {}, {lazy: true}); // lazy: true 表明計(jì)算屬性的watcher
Object.defineProperty(vm, key, {
get: createComputedGetter(vm, key)
})
}
}
let id = 0;
class Watcher {
constructor(vm , exprOrFn, cb = () => {}, opts = {}) {
this.vm = vm;
this.exprOrFn = exprOrFn;
if(typeof exprOrFn === 'function') { // 渲染watcher的時(shí)候執(zhí)行vm._update()渲染頁面
this.getter = exprOrFn;
} else {
this.getter = function () { // 用戶watcher的時(shí)候?yàn)榱双@取到oldValue和newValue
return getValue(exprOrFn, vm)
}
}
if(opts.user) {
this.user = true;
}
this.lazy = opts.lazy;
this.dirty = opts.lazy; // 和計(jì)算屬性的緩存相關(guān),判斷需不需要重新計(jì)算, 默認(rèn)為true是需要重新計(jì)算
this.cb = cb;
this.opts = opts;
this.depsId = new Set()
this.deps = []
this.id = id++; // 每次實(shí)例化watcher的時(shí)候id++,可以通過不同的id區(qū)分不同的watcher
// 計(jì)算屬性一開始不去獲取值
this.value = this.lazy ? undefined :this.get(); // 老的oldValue
}
get() {
pushTarget(this); // Dep.target = 當(dāng)前的watcher
let value = this.getter.call(this.vm);
popTarget();
return value;
}
evaluate() {
this.value = this.get();
this.dirty = false;
}
update() {
if(this.lazy) { // lazy判斷計(jì)算屬性的watcher
this.dirty = true; // dirty判斷是否重新取值
} else {
// 批量更新
queueWatcher(this); // 防止數(shù)據(jù)批量改變的時(shí)候執(zhí)行多次
// this.get(); // 直接更新,當(dāng)數(shù)據(jù)批量改變的時(shí)候會(huì)執(zhí)行多次
}
}
run() {
let value = this.get(); // 新的newValue
if(this.value !== value) {
this.cb(value, this.value)
}
}
depend() {
// 作用把watcher里面的addDep執(zhí)行
let i = this.deps.length;
while(i--) {
this.deps[i].depend();
}
}
// watcher和dep互相依賴
addDep(dep) {
let id = dep.id;
if(!this.depsId.has(id)) {
this.depsId.add(id);
this.deps.push(dep); // 把dep存在了watcher中
dep.addSub(this) // 把watcher存在dep中
}
}
}
計(jì)算屬性初始化的時(shí)候,需要遍歷每一個(gè)值,每一個(gè)計(jì)算屬性都有一個(gè)對(duì)應(yīng)的實(shí)例化
wathcer
,這個(gè)watcher是ComputedWatcher
。計(jì)算屬性和data,watch不同的是,他不能通過vm.
的方式取到值,所以也要進(jìn)行攔截。當(dāng)執(zhí)行$mount
的時(shí)候,先實(shí)例化一個(gè)RenderWatcher
,執(zhí)行pushTarget
,此時(shí)的Dep.target
指向的是當(dāng)前的RenderWatcher
,在執(zhí)行g(shù)etter的時(shí)候取vm.mm
的值,因?yàn)檫M(jìn)行了getter攔截處理,所以會(huì)執(zhí)行watcher.evaluate
,此時(shí)的Dep.target
指向的是當(dāng)前的ComputedWatcher
然后執(zhí)行popTarget操作,此Dep.target指向了
RenderWatcher
,這時(shí)候存在一個(gè)問題,此時(shí)的依賴中update函數(shù)是不能渲染頁面的,所以要辦法在dep中存入RenderWatcher
,辦法就是執(zhí)行ComputedWatcher
的depend
函數(shù),經(jīng)過這個(gè)操作之后message
和msg
,都分別保存了兩個(gè)依賴,數(shù)據(jù)改變的時(shí)候,視圖也會(huì)改變
小結(jié)
Observer
- 使用
Object.defineProperty
把data數(shù)據(jù)變成響應(yīng)式對(duì)象,使每一個(gè)對(duì)象都對(duì)應(yīng)的實(shí)例化dep
- 在getter中進(jìn)行依賴的收集,即
dep.depend()
- 在setter中進(jìn)行派發(fā)更新,即
dep.notify()
Dep
- 它
Watcher
實(shí)例的管理器 -
Dep.target
指向的是當(dāng)前的Watcher
實(shí)例 -
depend()
方法會(huì)將當(dāng)前的dep實(shí)例保存到watcher
中,也會(huì)保存當(dāng)前的watcher
Watcher
- 它是一個(gè)觀察者,數(shù)據(jù)改變的時(shí)候執(zhí)行更新操作
- 有三種
RenderWatcher
、UserWatcher
、ComputedWatcher
- 數(shù)據(jù)變 -> 使用數(shù)據(jù)的視圖變 ->
RenderWatcher
- 數(shù)據(jù)變 -> 使用數(shù)據(jù)的計(jì)算屬性變 -> 使用計(jì)算屬性的視圖變 ->
ComputedWatcher
- 數(shù)據(jù)變 -> 開發(fā)者主動(dòng)注冊(cè)的回調(diào)函數(shù)執(zhí)行 ->
UserWatcher
模版編譯及v-model的簡單實(shí)現(xiàn)
這里不涉及虛擬DOM等復(fù)雜邏輯,只是簡單替換
Vue.prototype._update = function() {
let vm = this;
let el = vm.$el;
// 不直接操作dom,把dom先保存到文件碎片里面,文件碎片是保存在內(nèi)存中的,把操作結(jié)果一次塞到dom中去
let node = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild) {
node.appendChild(firstChild);
}
// 編譯 處理{{ message }} -> vm.message
compiler(node, vm);
el.appendChild(node)
}
把
el
下的節(jié)點(diǎn)都保存到文件碎片中,經(jīng)過編譯之后差值表達(dá)式里面的內(nèi)容會(huì)被替換成真正的數(shù)據(jù),在把結(jié)果添加到dom上文章來源:http://www.zghlxwxcb.cn/news/detail-409466.html
const reg = /\{\{((?:.|\r?\n)+?)\}\}/g; // 任意字符或者換行 ?: 表是不捕獲分組
export function compiler (node, vm) {
let childNodes = node.childNodes;
// 把類數(shù)組轉(zhuǎn)換成數(shù)組
let childNodesArray = [...childNodes];
childNodesArray.forEach(child => {
if(child.nodeType === 1) {
if(child.hasAttribute('v-model')) {
const vmKey = child.getAttribute('v-model').trim();
const value = getValue(vmKey, vm);
child.value = value;
child.addEventListener('input', () => {
const keyArr = vmKey.split('.')
const obj = keyArr.slice(0, keyArr.length - 1).reduce((newObj, k) => newObj[k], vm)
const leafKey = keyArr[keyArr.length - 1]
obj[leafKey] = child.value;
})
}
// 元素節(jié)點(diǎn)
compiler(child, vm)
} else if (child.nodeType === 3) {
// 文本節(jié)點(diǎn)
compilerText(child, vm)
}
})
}
export function compilerText (node, vm) {
if(!node.expr) {
node.expr = node.textContent; // 把{{ message }} 保存,防止改變數(shù)據(jù)的時(shí)候匹配不到
}
node.textContent = node.expr.replace(reg, function(...args){
// ['{{ message }}', ' message ', 5, '\n {{ message }}\n ']
let key = trimSpace(args[1])
return JSON.stringify(getValue(key,vm))
})
}
export function getValue(expr,vm) {
// obj.a
let keys = expr.split('.');
return keys.reduce((prevValue, curValue) => {
prevValue = prevValue[curValue]
return prevValue
}, vm);
}
// 去掉空格
export function trimSpace(str) {
return str.replace(/\s+/g, '')
}
- 對(duì)節(jié)點(diǎn)進(jìn)行循環(huán)處理,發(fā)現(xiàn)文本節(jié)點(diǎn),進(jìn)行差值表達(dá)式的替換,通過reduce完成鏈?zhǔn)饺≈担瑢⒉逯堤鎿Q成真正的值
- 發(fā)現(xiàn)元素節(jié)點(diǎn),看節(jié)點(diǎn)上是否存在
v-model
屬性,獲取到v-model
綁定的鍵值,賦值給input
節(jié)點(diǎn),并且監(jiān)聽該節(jié)點(diǎn)的input
事件,將改變的值重新復(fù)制給數(shù)據(jù),賦值的過程,又會(huì)觸發(fā)dep.notify
,實(shí)現(xiàn)了視圖的改變影響數(shù)據(jù)的改變。
文章來源地址http://www.zghlxwxcb.cn/news/detail-409466.html
到了這里,關(guān)于vue2雙向數(shù)據(jù)綁定基本原理的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!