VUE 响应式原理源码:带你一步精通 VUE | 原力计划
import { arrayMethods } from './array' //数组变异方法处理
class Observer {
constructor(data) {
//用于对数组进行处理,存放数组的观察者watcher
this.dep = new Dep()
if (Array.isArray(data)) {
//如果是数组,使用数组的变异方法
data.__proto__ = arrayMethods
//把数组数据添加 __ob__ 一个Observer,当使用数组变异方法时,可以更新视图
data.__ob__ = this
//给数组的每一项添加数据劫持(setter/getter处理)
this.observerArray(data)
} else {
//非数组数据添加数据劫持(setter/getter处理)
this.walk(data)
}
}
}
// 获取Array的原型链
const arrayProto = Array.prototype;
// 重新创建一个含有对应原型的对象,在下面称为新Array
const arrayMethods = Object.create(arrayProto);
// 处理7个数组变异方法
['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'].forEach(ele => {
//修改新Array的对应的方法
arrayMethods[ele] = function () {
// 执行数组的原生方法,完成其需要完成的内容
arrayProto[ele].call(this, ...arguments)
// 获取Observer对象
const ob = this.__ob__
// 更新视图
ob.dep.notify()
}
})
export {
arrayMethods
}
//循环遍历数组,为数组每一项设置setter/getter
observerArray(items) {
for (let i = 0; i < items.length; i++) {
this.observer(items[i])
}
}
walk(data) {
//数据劫持
if (data && typeof data === "object") {
for (const key in data) {
//绑定setter和getter
this.defineReactive(data, key, data[key])
}
}
}
//数据劫持,设置 setter/getteer
defineReactive(data, key, value) {
//如果是数组的话,需要接受返回的Observer对象
let arrayOb = this.observer(value)
//创建订阅者/收集依赖
const dep = new Dep()
//setter和getter处理
Object.defineProperty(data, key, {
//可枚举的
enumerable: true,
//可修改的
configurable: false,
get() {
//当 Dep 有 watcher 时, 添加 watcher
Dep.target && dep.addSubs(Dep.target)
//如果是数组,则添加上数组的观察者
Dep.target && arrayOb && arrayOb.dep.addSubs(Dep.target)
return value
},
set: (newVal) => {
//新旧数据不相等时更改
if (value !== newVal) {
//为新设置的数据添加setter/getter
arrayOb = this.observer(newVal);
value = newVal
//通知 dep 数据发送了变化
dep.notify()
}
}
})
}
}
// 订阅者收集器
export default class Dep {
constructor() {
//管理的watcher的数组
this.subs = []
}
addSubs(watcher) {
//添加watcher
this.subs.push(watcher)
}
notify() {
//通知watcher更新dom
this.subs.forEach(w => w.update())
}
}
import Dep from './dep'
import { complieUtils } from './utils'
export default class Watcher {
constructor(vm, expr, cb) {
//当前的vue实例
this.vm = vm;
//表达式
this.expr = expr;
//回调函数,更新dom
this.cb = cb
//获取旧的数据,此时获取旧值的时候,Dep.target会绑定上当前的this
this.oldVal = this.getOldVal()
}
getOldVal() {
//将当前的watcher绑定起来
Dep.target = this
//获取旧数据
const oldVal = complieUtils.getValue(this.expr, this.vm)
//绑定完成后,将绑定的置空,防止多次绑定
Dep.target = null
return oldVal
}
update() {
//更新函数
const newVal = complieUtils.getValue(this.expr, this.vm)
if (newVal !== this.oldVal || Array.isArray(newVal)) {
//条用更新在compile中创建watcher时传入的回调函数
this.cb(newVal)
}
}
}
class Complie {
constructor(el, vm) {
this.el = this.isNodeElement(el) ? el : document.querySelector(el);
this.vm = vm;
// 1、将所有的dom对象放到fragement文档碎片中,防止重复操作dom,消耗性能
const fragments = this.nodeTofragments(this.el)
// 2、编译模板
this.complie(fragments)
// 3、追加子元素到根元素
this.el.appendChild(fragments)
}
}
complie(fragments) {
//获取所有节点
const nodes = fragments.childNodes;
[...nodes].forEach(ele => {
if (this.isNodeElement(ele)) {
//1. 编译元素节点
this.complieElement(ele)
} else {
//编译文本节点
this.complieText(ele)
}
//如果有子节点,循环遍历,编译指令
if (ele.childNodes && ele.childNodes.length) {
this.complie(ele)
}
})
}
complieElement(node) {
//1.获取所有的属性
const attrs = node.attributes;
//2.筛选出是属性的
[...attrs].forEach(attr => {
//attr是一个对象,name是属性名,value是属性值
const {name,value} = attr
//判断是否含有v-开头 如:v-html
if (name.startsWith("v-")) {
//将指令分离 text, html, on:click
const [, directive] = name.split("-")
//处理on:click或bind:name的情况 on,click
const [dirName, paramName] = directive.split(":")
//编译模板
complieUtils[dirName](node, value, this.vm, paramName)
//删除属性,在页面中的dom中不会再显示v-html这种指令的属性
node.removeAttribute(name)
} else if (name.startsWith("@")) {
// 如果是事件处理 @click='handleClick'
let [, paramName] = name.split('@');
complieUtils['on'](node, value, this.vm, paramName);
node.removeAttribute(name);
} else if (name.startsWith(":")) {
// 如果是事件处理 :href='...'
let [, paramName] = name.split(':');
complieUtils['bind'](node, value, this.vm, paramName);
node.removeAttribute(name);
}
})
}
complieText(node) {
//1.获取所有的文本内容
const text = node.textContent
//匹配{{}}
if (/\{\{(.+?)\}\}/.test(text)) {
//编译模板
complieUtils['text'](node, text, this.vm)
}
}
import Watcher from './watcher'
export const complieUtils = {
//处理text指令
text(node, expr, vm) {
let value;
if (/\{\{.+?\}\}/.test(expr)) {
//处理 {{}}
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
//绑定观察者/更新函数
new Watcher(vm, args[1], () => {
//第二个参数,传入回调函数
this.updater.updaterText(node, this.getContentVal(expr, vm))
})
return this.getValue(args[1], vm)
})
} else {
//v-text
new Watcher(vm, expr, (newVal) => {
this.updater.updaterText(node, newVal)
})
//获取到value值
value = this.getValue(expr, vm)
}
//调用更新函数
this.updater.updaterText(node, value)
},
}
//通过表达式, vm获取data中的值, person.name
getValue(expr, vm) {
return expr.split(".").reduce((data, currentVal) => {
return data[currentVal]
}, vm.$data)
},
//更新dom元素的方法
updater: {
//更新文本
updaterText(node, value) {
node.textContent = value
}
}
//处理model指令
model(node, expr, vm) {
const value = this.getValue(expr, vm)
//绑定watcher
new Watcher(vm, expr, (newVal) => {
this.updater.updaterModel(node, newVal)
})
//双向数据绑定
node.addEventListener("input", (e) => {
//设值方法
this.setVal(expr, vm, e.target.value)
})
this.updater.updaterModel(node, value)
},
//通过表达式,vm,输入框的值,实现设置值,input中v-model双向数据绑定
setVal(expr, vm, inputVal) {
expr.split(".").reduce((data, currentVal) => {
data[currentVal] = inputVal
}, vm.$data)
},
class Vue {
constructor(options) {
//获取模板
this.$el = options.el;
//获取data中的数据
this.$data = options.data;
//将对象中的属性存起来,以便后续使用
this.$options = options
//1.数据劫持,设置setter/getter
new Observer(this.$data)
//2.编译模板,解析指令
new Complie(this.$el, this)
}
}
export default class Vue {
constructor(options) {
//...
//1.数据劫持,设置setter/getter
//2.编译模板,解析指令
if (this.$el) { //如果有模板
//代理this
this.proxyData(this.$data)
}
}
proxyData(data) {
for (const key in data) {
//将当前的数据放到全局指向中
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newVal) {
data[key] = newVal
}
})
}
}
}
<div id="app">{{ name }}</div>
<script>
const vm = new Vue({
el: '#app',
data: {
name: 'monk'
}
})
vm.name = 'the young monk';
console.log(vm.name); // the young monk 此时数据已更改
console.log(vm.$el.innerHTML); // monk 此时页面还未重新渲染
// 1. 使用vm.$nextTick
vm.$nextTick(() => {
console.log(vm.$el.innerHTML); // the young monk 此时数据已更改
})
// 2. 使用Vue.nextTick
Vue.nextTick(() => {
console.log(vm.$el.innerHTML); // the young monk 此时数据已更改
})
</script>
Vue.nextTick(function () {
console.log(this); // window
})
vm.$nextTick(function () {
console.log(this); // vm实例
})
if(typeof Promise !== 'undefined') {
// 微任务
// 首先看一下浏览器中有没有promise
// 因为IE浏览器中不能执行Promise
const p = Promise.resolve();
} else if(typeof MutationObserver !== 'undefined') {
// 微任务
// 突变观察
// 监听文档中文字的变化,如果文字有变化,就会执行回调
// vue的具体做法是:创建一个假节点,然后让这个假节点稍微改动一下,就会执行对应的函数
} else if(typeof setImmediate !== 'undefined') {
// 宏任务
// 只在IE下有
} else {
// 宏任务
// 如果上面都不能执行,那么则会调用setTimeout
}
更多精彩推荐
☞避坑!使用 Kubernetes 最易犯的 10 个错误
☞
☞
你点的每个“在看”,我都认真当成了喜欢