vue2的双向数据绑定(又称响应式)原理,是通过数据劫持结合发布订阅模式的方式来实现的,通过Object.defineProperty()
来劫持各个属性的setter
,getter
,在数据变动时发布消息给订阅者,触发相应的监听回调来渲染视图。也就是说数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变。
Object.defineProperty
- 第一个参数 object对象
- 第二个参数 属性名
- 第三个参数 属性描述符 这里只介绍
get
和set
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
get() {
console.log('此处进行依赖的收集')
return value
},
set(newValue) {
if(newValue === value) return;
console.log('此处通知变化,执行更新操作')
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中的每一项变成响应式数据
defineReactive(data,key, value)
}
console.log(data.msg);
data.message = 'nihao'
从这个例子我们可以简单理解一下数据劫持,当我们用到了某个数据(比如在页面上、在计算属性中、在watch中),那么肯定会对这个数据进行访问,所以只要在getter中对数据的使用进行拦截,对这个数据的使用(也就是此数据的依赖)进行收集,收集好之后方便我们进行后续的处理。当我们改变某个数据的时候,需要用setter进行拦截,对我们在getter里面收集到的依赖进行响处理,来改变视图。
双向数据绑定及模版编译
vue初始化
- 初始化数据
- 初始化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) {
// 保存实例
let vm = this;
vm.$options = options;
initState(vm);
if(vm.$options.el) {
vm.$mount();
}
}
渲染Watcher(RenderWatcher)依赖的收集
初始化data
- 观察数据,把data下对象都变成响应式数据
- 每一个响应式数据,都有一个对应的实例化dep,作用是对依赖进行收集以及数据改变更新视图
Observer
export function defineReactive(data, key, value) {
let childOb = observe(value) // 递归把对象变成响应式
let dep = new Dep(key);
// 不兼容IE8及以下
Object.defineProperty(data, key, {
get() {
if(Dep.target) {
dep.depend();
if(childOb) {
childOb.dep.depend(); // 数组的依赖收集
dependArray(value)
}
}
return value;
},
set(newValue) {
if(newValue === value) return
observe(newValue)
value = newValue
dep.notify()
}
});
}
class Observer {
constructor(data) {
this.dep = new Dep();
// 为了让数组上有dep
Object.defineProperty(data, '__ob__', {
get: () => this
})
if(Array.isArray(data)) {
// 数组单独处理
data.__proto__ = newArrayProtoMethods;
observeArray(data); // 对数组本身存在的对象进行观察
} 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]
// 变成响应式数据
defineReactive(data, key, value)
}
}
}
Dep
- 对响应数据的依赖进行收集
- 数据改变时,对依赖进行update处理
-
depend
函数使dep
和watcher
相互依赖
let id = 0;
class Dep {
constructor() {
this.id = id++;
this.subs = [];
}
// 发布订阅模式
// 订阅
addSub(watcher) {
this.subs.push(watcher)
}
// 发布
notify() {
this.subs.forEach(watcher => watcher.update());
}
// 会把watcher存到dep当中,还会把dep存到
depend() {
if(Dep.target) {
Dep.target.addDep(this);
}
}
}
let stack = [];
// watcher进栈
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
- 实例化一个RenderWatcher,传入当前的实例和页面渲染函数
-
_update
里面涉及到模版编译,我们可以先忽略模版编译的内容,只需要知道模版编译的时候要取实例上的对象比如vm.message
Vue.prototype.$mount = function() {
let vm = this;
let el = vm.$options.el;
el = vm.$el = query(el);
// 首次渲染组件或者因为数据变化重新渲染组件
let updateComponent = () => {
vm._update();
}
// 渲染watcher
new Watcher(vm, updateComponent);
}
Vue.prototype._update = function() {
let vm = this;
let el = vm.$el;
// 不直接操作dom,把dom先保存到文件碎片里面,文件碎片是保存在内存中的,把操作结果一次塞到dom中去
let node = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild) {
node.appendChild(firstChild);
}
// 编译 处理{{ message }} -> vm.message -> 触发message的getter -> 进行依赖的收集
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++; // 每次实例化watcher的时候id++,可以通过不同的id区分不同的watcher
this.get();
}
get() {
pushTarget(this); // Dep.target = 当前的watcher
this.getter();
popTarget();
return value;
}
update() {
this.get();
}
depend() {
// 作用把watcher里面的addDep执行
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中
}
}
}
执行getter的时候,先把当前的
RenderWatcher
push到stack
中,并且把Dep.target
设置为当前的renderWatcher
。其次时执行渲染函数,取实例上的值vm.message
,这时候会走到message
的setter
,执行dep.depend()
操作,depend
会把当前的dep
存在RenderWatcher
中,也会把当前的renderWatcher
存在dep
中。执行完后把RenderWatcher
pop掉,并且把Dep.target
指向前一个依赖。
改变数据
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());
}
改变响应式数据的时候,会在
setter
里面进行拦截,执行dep.notify
,他会循环执行dep.subs
里面保存watcher
(即依赖)的update
函数,这里等同于get
函数,又会执行同上的操作,数据的改变会导致视图的变化。
用户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; // 拿到实例上的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 区分于渲染watcher
}
初始化
watch
的时候,会遍历每一个属性,每一个属性都有一个实例化的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++; // 每次实例化watcher的时候id++,可以通过不同的id区分不同的watcher
this.value = this.get(); // 老的oldValue
}
get() {
pushTarget(this); // Dep.target = 当前的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执行
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中
}
}
}
实例化UserWatcher的时候,执行
get
,此时Dep.target
指向UserWatcher
,并且stack
中push这个UserWatcher
,getValue
的时候会取值vm.message
,所以会执行dep.depend
进行依赖的收集,此时message的dep中就存在这个userWatcher
了。之后把UserWatcher
从stack
中pop掉。
当3s钟后数据改变的时候,setter拦截,dep.notify(),将messge的dep中subs遍历执行依赖的update,又要执行pushTarget、getter、popTareget等操作
计算属性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]; // 当前的计算属性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); // 创建空对象,为了在实例里面更加方便的访问到计算属性的watcher
for (let key in computed) {
let userDef = computed[key];
watchers[key] = new Watcher(vm, userDef, () => {}, {lazy: true}); // lazy: true 表明计算属性的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的时候执行vm._update()渲染页面
this.getter = exprOrFn;
} else {
this.getter = function () { // 用户watcher的时候为了获取到oldValue和newValue
return getValue(exprOrFn, vm)
}
}
if(opts.user) {
this.user = true;
}
this.lazy = opts.lazy;
this.dirty = opts.lazy; // 和计算属性的缓存相关,判断需不需要重新计算, 默认为true是需要重新计算
this.cb = cb;
this.opts = opts;
this.depsId = new Set()
this.deps = []
this.id = id++; // 每次实例化watcher的时候id++,可以通过不同的id区分不同的watcher
// 计算属性一开始不去获取值
this.value = this.lazy ? undefined :this.get(); // 老的oldValue
}
get() {
pushTarget(this); // Dep.target = 当前的watcher
let value = this.getter.call(this.vm);
popTarget();
return value;
}
evaluate() {
this.value = this.get();
this.dirty = false;
}
update() {
if(this.lazy) { // lazy判断计算属性的watcher
this.dirty = true; // dirty判断是否重新取值
} else {
// 批量更新
queueWatcher(this); // 防止数据批量改变的时候执行多次
// this.get(); // 直接更新,当数据批量改变的时候会执行多次
}
}
run() {
let value = this.get(); // 新的newValue
if(this.value !== value) {
this.cb(value, this.value)
}
}
depend() {
// 作用把watcher里面的addDep执行
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中
}
}
}
计算属性初始化的时候,需要遍历每一个值,每一个计算属性都有一个对应的实例化
wathcer
,这个watcher是ComputedWatcher
。计算属性和data,watch不同的是,他不能通过vm.
的方式取到值,所以也要进行拦截。当执行$mount
的时候,先实例化一个RenderWatcher
,执行pushTarget
,此时的Dep.target
指向的是当前的RenderWatcher
,在执行getter的时候取vm.mm
的值,因为进行了getter拦截处理,所以会执行watcher.evaluate
,此时的Dep.target
指向的是当前的ComputedWatcher
然后执行popTarget操作,此Dep.target指向了
RenderWatcher
,这时候存在一个问题,此时的依赖中update函数是不能渲染页面的,所以要办法在dep中存入RenderWatcher
,办法就是执行ComputedWatcher
的depend
函数,经过这个操作之后message
和msg
,都分别保存了两个依赖,数据改变的时候,视图也会改变
小结
Observer
- 使用
Object.defineProperty
把data数据变成响应式对象,使每一个对象都对应的实例化dep
- 在getter中进行依赖的收集,即
dep.depend()
- 在setter中进行派发更新,即
dep.notify()
Dep
- 它
Watcher
实例的管理器 -
Dep.target
指向的是当前的Watcher
实例 -
depend()
方法会将当前的dep实例保存到watcher
中,也会保存当前的watcher
Watcher
- 它是一个观察者,数据改变的时候执行更新操作
- 有三种
RenderWatcher
、UserWatcher
、ComputedWatcher
- 数据变 -> 使用数据的视图变 ->
RenderWatcher
- 数据变 -> 使用数据的计算属性变 -> 使用计算属性的视图变 ->
ComputedWatcher
- 数据变 -> 开发者主动注册的回调函数执行 ->
UserWatcher
模版编译及v-model的简单实现
这里不涉及虚拟DOM等复杂逻辑,只是简单替换
Vue.prototype._update = function() {
let vm = this;
let el = vm.$el;
// 不直接操作dom,把dom先保存到文件碎片里面,文件碎片是保存在内存中的,把操作结果一次塞到dom中去
let node = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild) {
node.appendChild(firstChild);
}
// 编译 处理{{ message }} -> vm.message
compiler(node, vm);
el.appendChild(node)
}
把
el
下的节点都保存到文件碎片中,经过编译之后差值表达式里面的内容会被替换成真正的数据,在把结果添加到dom上文章来源:https://www.toymoban.com/news/detail-409466.html
const reg = /\{\{((?:.|\r?\n)+?)\}\}/g; // 任意字符或者换行 ?: 表是不捕获分组
export function compiler (node, vm) {
let childNodes = node.childNodes;
// 把类数组转换成数组
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;
})
}
// 元素节点
compiler(child, vm)
} else if (child.nodeType === 3) {
// 文本节点
compilerText(child, vm)
}
})
}
export function compilerText (node, vm) {
if(!node.expr) {
node.expr = node.textContent; // 把{{ message }} 保存,防止改变数据的时候匹配不到
}
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, '')
}
- 对节点进行循环处理,发现文本节点,进行差值表达式的替换,通过reduce完成链式取值,将插值替换成真正的值
- 发现元素节点,看节点上是否存在
v-model
属性,获取到v-model
绑定的键值,赋值给input
节点,并且监听该节点的input
事件,将改变的值重新复制给数据,赋值的过程,又会触发dep.notify
,实现了视图的改变影响数据的改变。
文章来源地址https://www.toymoban.com/news/detail-409466.html
到了这里,关于vue2双向数据绑定基本原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!