# T【手写vue案例】
目标:数据响应式+模板解析+渲染
第一步:数据驱动,最初版的响应式视图更新,需要手动书写update方法,手动操作dom更新,框架就是为了解决用户去调用和编写update,而且下面的写法只能更改一个值,局限性太大
<div id="app">
{{foo}}
</div>
<script>
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
// console.log('get', key);
return val
},
set(v) {
if (val !== v) {
val = v
update()
}
},
})
}
function update() {
app.innerText = obj.foo
}
const obj = {}
defineReactive(obj, 'foo', 'foo')
setInterval(() => {
obj.foo = new Date().toLocaleTimeString()
}, 1000)
</script>
第二步:①因为不能批量记录,所以需要实现,用observe方法遍历地给每一个obj.key添加拦截器监听;②同时,为了实现嵌套情况的情形,在步骤【0】额外调用一次observe处理可能出现的递归;③ 在obj.defineProperty中,当设置值的时候,可能不是obj.key=xxx形式,而是obj={key:xxx};所以在步骤【1】仍然需要对新值进行一次observe处理; ④ 动态添加的属性也需要去监听,vue中使用set,在这里也可以定义一个set方法,动态调用一次defineReactive函数增加一个监听步骤【2】
// 将传入的obj,动态设置一个key,它的值val
function defineReactive(obj, key, val) {
// 递归
observe(val)//【0】
Object.defineProperty(obj, key, {
get() {
console.log('get', key);
return val
},
set(v) {
if (val !== v) {
console.log('set', key);
// 传入新值v可能还是对象
observe(v)//【1】
val = v
}
},
})
}
// 递归遍历obj,动态拦截obj的所有key
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return obj
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
// this.$set()
// Vue.set()
function set(obj, key, val) {
defineReactive(obj, key, val)//【2】
}
const obj = {
foo: 'foo',
bar: 'bar',
baz: {
a: 1
}
}
// defineReactive(obj, 'foo', 'foo')
observe(obj)
// obj.foo
// obj.foo = 'fooooooo'
// obj.baz.a
// obj.baz = { a: 10 }
// obj.baz.a
// obj.dong = 'dong'
// obj.dong
// set(obj, 'dong', 'dong')
// obj.dong
KVue:框架构造函数
Observer:执行数据响应化(分辨数据是对象还是数组)
Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)
Watcher:执行更新函数(更新dom)
Dep:管理多个Watcher,批量更新
创建class,constructor传入需要的el,data,methods等参数
data代理到this上
A. 具体实现代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<div id="app">
<!-- 插值绑定 -->
<p>{{name}}</p>
<!-- 指令解析 -->
<p k-text="name"></p>
<p>{{age}}</p>
<p>
{{doubleAge}}
</p>
<!-- 双向绑定 -->
<input type="text" k-model="name" />
<!-- 事件处理 -->
<button @click="changeName">change</button>
<!-- html内容解析 -->
<div k-html="html"></div>
<div>{{arr}}</div>
</div>
<script src="./compile.js"></script>
<script src="./kvue.js"></script>
<script>
const k = new KVue({
el: "#app",
data: {
name: "I am test.",
age: 12,
html: "<button>这是一个按钮</button>",
arr:[1,2,3,4]
},
created() {
console.log("开始啦");
},
methods: {
changeName() {
this.name = "hello";
this.age++
}
}
});
</script>
</body>
</html>
//kvuejs
//(1)数据的监听
//(2)依赖的收集
// 依赖收集的原因:当发现数据变化后,需要对页面进行更新
// 在程序收集之前需要对templete进行遍历收集,找出和数据有依赖的东西,收集保存下来,需要更新的时候进行处理
// 两个对象,一个是dep 和 watcher
class KVue {
constructor(options) {
this.$options = options
//数据响应化
this.$data = this.$options.data
//遍历数据,所有的数据都进行get和set的遍历
this.observe(this.$data)
new Compile(options.el, this)
//created 执行
if(options.created) {
options.created.call(this)
}
}
//对所有的数据进行观察和监听
observe(value) {
if(!value || typeof value !== 'object') {
return
}
Object.keys(value).forEach(key => {
//响应式处理
this.defineReactive(value,key,value[key])
//代理data中的属性到vue实例上
this.proxyData(key)
})
}
//数据的响应化
defineReactive(obj, key, val) {
this.observe(val) //递归解决数据格式的问题,对象里面包含对象
//模拟一下watcher的创建过程
//每一个dep实例和data中每个key与一一对应关系
const dep = new Dep()
//给object的每一个key定义拦截
Object.defineProperty(obj, key, {
get() {//如果是调用,直接返回val
//依赖收集
// console.log(`defineProperty-2-${key}`)
Dep.target && dep.addDep(Dep.target)
// console.warn(`这个data.${key}的属性里添加了${dep.length()}个`)
return val
},
set(newVal) {
if(newVal === val) {
return
}
val = newVal //key的值发生了变化,此时需要更新视图了
dep.notify()
}
})
}
proxyData(key) {
Object.defineProperty(this, key, {
get() {
//this[key] 相当于 this.$data[key]
return this.$data[key]
},
set(newVal) {
this.$data[key] = newVal
}
})
}
}
//用来管理watcher 的,是一个观察者的的管家
class Dep {
constructor() {
this.watchers= [] //存储若干个依赖,一个watcher对应一个属性
}
addDep(dep) { //添加依赖的方法的
this.watchers.push(dep)
// console.log(this.watchers)
}
length(){
return this.watchers.length
}
notify() { //通知所有的依赖,做更新
this.watchers.forEach(item => { //遍历所有的东西,进行更新
// console.log(`notify-${item}-4`)
item.update()
})
}
}
//用来做调用更新的一个东西,保存data中的数值和页面的挂钩关系
class Watcher {
constructor(vm, key, cb) {
// console.log(`watcher-3-${key}`)
this.vm = vm
this.key = key
this.cb = cb
//将当前的Watcher实例指定到Dep静态的target
//---> 每次new Watcher()时,都会给Dep.target赋予一个new Watcher()出来的对象,便于依赖的收集
Dep.target = this
// console.log(Dep.target)
this.vm[this.key] //触发getter,添加依赖
Dep.target = null //置空
}
update() {
this.cb.call(this.vm, this.vm[this.key])
}
}
//compile.js
//编译器
// 用法 new Compile(el, vm) el 要遍历和分析的元素 vm指定现在vue的实例
class Compile {
constructor(el, vm) {
//el的可能是一个选择器,也可能是一个元素,需要分析 ---》这里不做探究
//要遍历的宿主节点
console.log(vm)//kvue的实例
this.$el = document.querySelector(el);
this.$vm = vm
//判断el的存在,存在的话需要进行编译
if(this.$el) {
//转换内部的内容为片段
this.$fragment = this.nodeToFragment(this.$el)
//执行编译
this.compile(this.$fragment)
//将编译完的结果html追加到$el
this.$el.appendChild(this.$fragment)
}
}
//将宿主元素中的代码片段拿出来遍历 ----》 高效
nodeToFragment(el) {
const frag = document.createDocumentFragment()
//将el中的所有的子元素搬家到frag中
let child;
while(child = el.firstChild) {
frag.appendChild(child) //移动节点
}
return frag
}
//编译过程 ---节点的类型 --》dom节点还是文本节点
compile(el) {
const childNode = el.childNodes;
Array.from(childNode).forEach(item => {
//节点的类型判断
if(this.isElement(item)){
//元素
// console.log('编译元素',item.nodeName)
const nodeAttrs = item.attributes;
//查找属性是k-,@,:
Array.from(nodeAttrs).forEach(attr => {
const attrName = attr.name //属性名
const exp = attr.value //属性值
if(this.isDirective(attrName)) {
//k-text
const dir = attrName.substring(2)
//普通指令 执行指令 text或者model
this[dir] && this[dir](item, this.$vm, exp)
}
if(this.isEvent(attrName)) {
const dir = attrName.substring(1)
this.eventHandler(item, this.$vm, exp, dir)
}
})
} else if(this.isInterpolation(item)) {
//文本
//console.log('编译文本',item.textContent)
//编译文本
this.compileText(item)
}
//递归子节点
if(item.childNodes && item.childNodes.length > 0) {
this.compile(item)
}
})
}
isElement(node) { //元素节点
return node.nodeType === 1
}
//插值文本
isInterpolation(node) { //即是文本又是插值语
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
//指令
isDirective(attr) {
return attr.indexOf('k-') == 0
}
//事件
isEvent(attr) {
return attr.indexOf('@') == 0
}
// k-etxt
text(node, vm, exp) {
this.update(node, vm, exp, 'text')
}
// @的处理 item, this.$vm, exp, dir
eventHandler(node, vm, exp, dir) {
let fn = vm.$options.methods && vm.$options.methods[exp]
if(dir && fn) {
node.addEventListener(dir, fn.bind(vm))
}
}
//双向绑定
model(node, vm, exp) {
this.update(node, vm, exp, 'model')
node.addEventListener('input', e => {
vm[exp] = e.target.value
})
}
//双向绑定的更新
modelUpdater(node, value) {
//input的value
node.value = value
}
//html
html(node, vm, exp) {
this.update(node, vm, exp, 'html')
node.addEventListener('input', e => {
vm[exp] = e.target.value
})
}
//双向绑定的更新
htmlUpdater(node, value) {
//input的value
node.innerHTML = value
}
//编译文本
compileText(node) {
// console.log(RegExp.$1)
// node.textContent = this.$vm.$data[RegExp.$1]
this.update(node, this.$vm, RegExp.$1, 'text')
}
//更新函数
update(node, vm, exp, dir) {
// console.log("compile-update-1")
//node -->节点 vm --> vue的实例 exp --> 表达式 dir --> 指令(更新的是文本还是指令)
const updaterFun = this[dir+'Updater']
//初始化
updaterFun && updaterFun(node,vm[exp])
//依赖收集
new Watcher(vm, exp, (value) => {
updaterFun && updaterFun(node,value)
})
}
textUpdater(node, value) {
node.textContent = value
}
}
# 观察者模式
// 观察者要直接订阅观察目标,观察目标一做出通知,观察者就要进行处理
// 观察者集合
class ObserverList {
constructor() {
this.list = [];
}
add(obj) {
console.log(obj) //Observer {update: ƒ}
this.list.push(obj);
}
removeAt(index) {
this.list.splice(index, 1);
}
count() {
return this.list.length;
}
get(index) {
if (index < 0 || index >= this.count()) {
return;
}
return this.list[index];
}
indexOf(obj, start = 0) {
let pos = start;
while (pos < this.count()) {
if (this.list[pos] === obj) {
return pos;
}
pos++;
}
return -1;
}
}
// 观察者类
class Observer {
constructor(fn) {
this.update = fn;
}
}
// 观察目标类
class Subject {
constructor() {
this.observers = new ObserverList();
}
addObserver(observer) {
this.observers.add(observer);
}
removeObserver(observer) {
this.observers.removeAt(
this.observers.indexOf(observer)
);
}
notify(context) {
const count = this.observers.count();
for (let i = 0; i < count; ++i) {
this.observers.get(i).update(context);
}
}
}
const observer = new Observer((newval) => {
console.log(`A的最新值是${newval}`);
})
const subject = new Subject();
subject.addObserver(observer);
subject.notify('Hello, world');
# 引入vnode概念手写vue
<div id="app">{{counter}}</div>
<script src="kvue.js"></script>
<script>
const app = new KVue({
el: '#app',
data: {
counter: 1
},
render(h) {
// const div = document.createElement('div')
// div.textContent = this.counter
// return div
// return h('div', null, this.counter + '')
return h('div', null, [
h('p', null, this.counter + ''),
h('p', null, this.counter * 2 + ''),
h('p', null, this.counter * 3 + ''),
])
}
})
setInterval(() => {
app.counter++
}, 1000);
</script>
// 实现KVue构造函数
function defineReactive(obj, key, val) {
// 如果val是对象,需要递归处理之
observe(val);
// 管家创建
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
console.log("get", key);
// 依赖收集
Dep.target && dep.addDep(Dep.target);
return val;
},
set(newVal) {
if (val !== newVal) {
// 如果newVal是对象,也要做响应式处理
observe(newVal);
val = newVal;
console.log("set", key, newVal);
// 通知更新
dep.notify();
}
}
});
}
// 遍历指定数据对象每个key,拦截他们
function observe(obj) {
if (typeof obj !== "object" || obj === null) {
return obj;
}
// 每遇到一一个对象,就创建一个Observer实例
// 创建个Observer实例去做拦截操作
new Observer(obj);
}
// proxy代理函数:让用户可以直接访问data中的key
function proxy(vm, key) {
Object.keys(vm[key]).forEach(k => {
Object.defineProperty(vm, k, {
get() {
return vm[key][k];
},
set(v) {
vm[key][k] = v;
}
});
});
}
// 根据传入value类型做不同操作
class Observer {
constructor(value) {
this.value = value;
// 判断一下value类型
// 遍历对象
this.walk(value);
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
}
class KVue {
constructor(options) {
// 0.保存options
this.$options = options;
this.$data = options.data;
// 1.将data做响应式处理
observe(this.$data);
// 2.为$data做代理
proxy(this, "$data");
// 3.编译模板
// new Compile('#app', this)
// 如果设置el,则挂载
if (options.el) {
this.$mount(options.el)
}
}
$mount(el) {
// 0.获取宿主
this.$el = document.querySelector(el)
// 1.声明一个updateComponent
const updateComponent = () => {
// const el = this.$options.render.call(this)
// const parent = this.$el.parentElement
// parent.insertBefore(el, this.$el.nextSibling)
// parent.removeChild(this.$el)
// this.$el = el
// vnode
const vnode = this.$options.render.call(this, this.$createElement)
this._update(vnode)
}
// 2.new Watcher
new Watcher(this, updateComponent)
}
// 创建返回虚拟dom
$createElement(tag, props, children) {
return {tag, props, children}
}
// 将传入的vnode转换,如果初始化则创建,如果是更新则patch
_update(vnode) {
// 上次vnode
const prevVnode = this._vnode
if (!prevVnode) {
// init
this.__patch__(this.$el, vnode)
} else {
// patch
this.__patch__(prevVnode, vnode)
}
this._vnode = vnode
}
__patch__(oldVnode, vnode) {
// 判断oldVnode是否是真实dom
if (oldVnode.nodeType) {
const parent = oldVnode.parentElement
const refElm = oldVnode.nextSibling
const el = this.createElm(vnode)
parent.insertBefore(el, refElm)
parent.removeChild(oldVnode)
// 保存vnode
this._vnode = vnode
} else {
// 获取要更新的元素
const el = vnode.el = oldVnode.el
// 同层比较相同节点
if (oldVnode.tag === vnode.tag) {
// diff
// props
// children
// 获取双方孩子
const oldCh = oldVnode.children
const newCh = vnode.children
if (typeof newCh === 'string') {
if (typeof oldCh === 'string') {
// 文本更新
if (newCh !== oldCh) {
el.textContent = newCh
}
} else {
// replace elements with text
el.textContent = newCh
}
} else {
if (typeof oldCh === 'string') {
// replace text with elements
// clear
// 循环创建并追加
} else {
this.updateChildren(el, oldCh, newCh)
}
}
} else {
// replace
}
}
}
updateChildren(parentElm, oldCh, newCh) {
// 这里暂且直接patch对应索引的两个节点
const len = Math.min(oldCh.length, newCh.length)
for (let i = 0; i < len; i++) {
this.__patch__(oldCh[i], newCh[i])
}
// newCh若是更长的那个,说明有新增
if (newCh.length > oldCh.length) {
newCh.slice(len).forEach(child => {
const el = this.createElm(child)
parentElm.appendChild(el)
})
} else if (newCh.length < oldCh.length) {
// oldCh若是更长的那个,说明有删减
oldCh.slice(len).forEach(child => {
parentElm.removeChild(child.el)
})
}
}
createElm(vnode) {
const el = document.createElement(vnode.tag)
// props
// children
if (vnode.children) {
if (typeof vnode.children === 'string') {
// 文本内容
el.textContent = vnode.children
} else {
// 子节点递归
vnode.children.forEach(vnode => {
el.appendChild(this.createElm(vnode))
})
}
}
// 保存真实元素,更新时要用
vnode.el = el
// 返回创建的dom
return el
}
}
// 移除
// class Compile {}
class Watcher {
constructor(vm, fn) {
this.vm = vm;
// this.key = key;
this.getter = fn;
this.get()
}
get(){
// 依赖收集触发
Dep.target = this;
this.getter.call(this.vm)
Dep.target = null;
}
update() {
this.get()
}
}
// 管家:和某个key,一一对应,管理多个秘书,数据更新时通知他们做更新工作
class Dep {
constructor() {
this.deps = new Set();
}
addDep(watcher) {
this.deps.add(watcher);
}
notify() {
this.deps.forEach(watcher => watcher.update());
}
}