背景
vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。接着,我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图,接下来实现过程
kvue.js
// 数据响应式
function defineReactive(obj, key, val) {
// 递归
observe(val);
// 创建一个对应的Dep实例
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
console.log("get", key);
// 依赖收集
Dep.target && dep.addDep(Dep.target)
return val;
},
set(newVal) {
if (newVal !== val) {
observe(newVal);
console.log("set", key);
val = newVal;
// update()
dep.notify()
}
},
});
}
// 递归遍历方法
function observe(obj) {
if (typeof obj !== "object" || obj === null) {
return;
}
// 创建Observer实例
new Observer(obj);
}
// 响应式对象中的某个key只要它的值是一个对象就要创建一个Observer实例
class Observer {
// 根据传入对象的类型做不同的响应式处理
constructor(obj) {
if (Array.isArray(obj)) {
// todo
} else {
// 对象响应式
this.walk(obj);
}
}
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
}
function proxy(vm) {
Object.keys(vm.$data).forEach((key) => {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key];
},
set(v) {
vm.$data[key] = v;
},
});
});
}
class KVue {
// new KVue({el, data})
constructor(options) {
this.$options = options;
this.$data = options.data;
// 1.对data做响应式处理
observe(this.$data);
// 1.5 代理
proxy(this);
// 2.编译
new Compile(options.el, this);
}
}
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
// 遍历宿主元素
if (this.$el) {
this.compile(this.$el);
}
}
compile(el) {
// 递归遍历根元素
el.childNodes.forEach((node) => {
if (this.isElm(node)) {
// console.log("编译元素", node.nodeName);
this.compileElement(node);
} else if (this.isInter(node)) {
// console.log('编译插值文本', node.textContent);
this.compileText(node);
}
// 递归
if (node.childNodes.length > 0) {
this.compile(node);
}
});
}
// 元素判断
isElm(node) {
return node.nodeType === 1;
}
// 插值判断
isInter(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
// {{ooxx}} => ooxx => this.$vm.ooxx
compileText(node) {
this.update(node, RegExp.$1, 'text')
}
// 元素的编译
compileElement(node) {
// 遍历所有属性:检查是否存在指令和事件
const attrs = node.attributes;
Array.from(attrs).forEach((attr) => {
// k-text="counter"
const attrName = attr.name; // k-text
const exp = attr.value; // counter
// 只处理动态值
// 指令-directive
if (this.isDir(attrName)) {
// 希望执行一个指令处理函数
const dir = attrName.substring(2);
this[dir] && this[dir](node, exp);
}
});
}
update(node, exp, dir) {
// 1.初始化
// 执行dir对应的实操函数
const fn = this[dir+'Updater']
fn && fn(node, this.$vm[exp])
// 2.创建Watcher实例
new Watcher(this.$vm, exp, function (val) {
fn && fn(node, val)
})
}
isDir(attrName) {
return attrName.startsWith("k-");
}
// k-text
text(node, exp) {
this.update(node, exp, 'text')
}
textUpdater(node, val) {
node.textContent = val;
}
//k-model
model(node, exp) {
this.update(node, exp, 'model')
//事件监听
node.addEventListener('input',e=>{
this.$vm[exp] = e.target.value
})
}
modelUpdater(node, val) {
node.value = val;
}
// k-html
html(node, exp) {
this.update(node, exp, 'html')
}
htmlUpdater(node, val) {
node.innerHTML = val;
}
}
// 更新执行者Watcher
class Watcher {
constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updater = updater
// 保存Watcher引用
Dep.target = this
this.vm[this.key]
Dep.target = null
}
update() {
this.updater.call(this.vm, this.vm[this.key])
}
}
class Dep {
constructor() {
this.deps = []
}
addDep(dep) {
this.deps.push(dep)
}
notify() {
this.deps.forEach(w => w.update())
}
}
khtml
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<div id="app">
<p @click="add">{{counter}}</p>
<p k-text="counter"></p>
<p k-html="desc"></p>
<input type="text" k-model="content">
<h2 k-html="content"></h2>
</div>
<script src="./kvue.js"></script>
<script>
const app = new KVue({
el: '#app',
data: {
counter: 1,
desc: '等待·<span style="color: red">真棒</span>',
content:'测试文案'
},
methods: {
add() {
this.counter++
}
},
})
setInterval(() => {
app.counter++
// app.$data.counter++
}, 1000);
</script>
vue 是如何对数组方法进行变异的
源码
import { def } from "../util/index";
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method];
//定义新的方法
def(arrayMethods, method, function mutator(...args) {
// args 传入的参数 Arr.push(33) args就是[33]
const result = original.apply(this, args);
const ob = this.__ob__;
console.log(ob,args);
let inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
}
if (inserted) ob.observeArray(inserted);
// notify change
ob.dep.notify();
return result;
});
});
// 在其他文件里 ,全局搜索就能看出来
// Observer.prototype.observeArray = function observeArray(items) {
// for (var i = 0, l = items.length; i < l; i++) {
// observe(items[i]);
// }
// };
简单来说,Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的ob,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 对新的值进行监听,然后手动调用 notify,通知 render watcher,执行 update
vue初始化流程
入口 platforms/web/entry-runtime-with-compiler.js
扩展默认$mount方法:处理template或el选项
platforms/web/runtime/index.js
- 安装web平台特有指令和组件
- 定义__patch__:补丁函数,执行patching算法进行更新
- 定义$mount:挂载vue实例到指定宿主元素(获得dom并替换宿主元素)
core/index.js
初始化全局api
具体如下:
- Vue.set = set
- Vue.delete = del
- Vue.nextTick = nextTick
- initUse(Vue) // 实现Vue.use函数
- initMixin(Vue) // 实现Vue.mixin函数
- initExtend(Vue) // 实现Vue.extend函数
- initAssetRegisters(Vue) // 注册实现Vue.component/directive/filter
core/instance/index.js
Vue构造函数定义
定义Vue实例API
function Vue (options) {
// 构造函数仅执行了_init this._init(options)
}
initMixin(Vue) // 实现init函数
stateMixin(Vue) // 状态相关api $data,$props,$set,$delete,$watch
eventsMixin(Vue)// 事件相关api $on,$once,$off,$emit
lifecycleMixin(Vue) // 生命周期api _update,$forceUpdate,$destroy
renderMixin(Vue)// 渲染api _render,$nextTick
core/instance/init.js
创建组件实例,初始化其数据、属性、事件等
- initLifecycle(vm) // root,refs
- initEvents(vm) // 处理父组件传递的事件和回调
- initRender(vm) // scopedSlots,_c,$createElement
- callHook(vm, 'beforeCreate')
- initInjections(vm) // 获取注入数据
- initState(vm) // 初始化props,methods,data,computed,watch
- initProvide(vm) // 提供数据注入
- callHook(vm, 'created')
$mount
- mountComponent 执行挂载,获取vdom并转换为dom
- new Watcher() 创建组件渲染watcher
- updateComponent() 执行初始化或更新
- update() 初始化或更新,将传入vdom转换为dom,初始化时执行的是dom创建操作
- render() src\core\instance\render.js 渲染组件,获取vdom
整体流程
new Vue() => _init() => $mount() => mountComponent() =>
new Watcher() => updateComponent() => render() => _update()