作为前端小白,日常搬砖,写的一般都是业务代码,对底层的实现原理一知半解。
so 觉得这样浑浑噩噩木有提升,开始从vue2.0源码入手,简单分析。最终实现一个简化版的Vue即可。
本篇文章不从源码入手,因为源码功能繁多,通过简单的几个案例和分析,实现深入了解,从而了解vue的实现原理
Vue 实现原理
核心:实现数据 响应式
理解Vue的设计思想:MVVM
Vue是基于MVVM的一个前端框架,so 我就从MVVM入手
MVVM
M:模型层,负责业务数据相关
V:视图层,负责视图相关,可以理解就是html+css层
VM:用以连接V与M,负责监听M或者V的修改,扫描数据、拦截感知、执行更新函数,下图 Data Bindings 让视图更新,反之通过事件触发 model 的数据,从而实现双向绑定数据劫持。
将视图View的状态和⾏为抽象化,让我们将视图 UI 和业务逻辑分开。
可以总结三点:数据响应式、模板引擎、渲染
1. 数据响应式说明:监听 数据变化并在视图中更新
实现方法:Object.defineProperty()
前置:创建一个对象obj, 设置一个属性foo为0,在视图层有个id为app的容器。
场景:if 当 obj.off 发生改变时,让视图内容跟着变化。简单实现数据变化并在视图中更新
// 响应式通过 Object.defineProperty()
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
get () { // 有变化就返回
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)
复制代码
由上例可知,obj对象 的参数映射到 dom,参数改变影响页面
对方法升级改造首先,我们触发更新的方法是defineReactive(obj, 'foo', 'foo')。这样并不理想,所以要定义objconst obj = {
foo: 'foo',
bar: 'bar',
baz: {
a: 1
}
}复制代码我去拦截obj的每一个key属性,并赋值。那么就需要递归,去遍及obj的值,写一个observe方法// 递归遍历obj,动态拦截obj的所有key
function observe(obj) {
if (typeof obj !== "object" || obj == null) { // 指定obj类型必须是对象
return obj;
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}复制代码
这样当触发 obj.foo = 'fooooo',会被拦截并赋值
但是又遇到问题obj.baz是个对象,无法拦截。所以我们在 defineReactive函数中去执行一次 observe 方法 对每个val再进行递归
直接操作某层级的对象。e.g.:obj.baz = {a: 10} 监听不到 obj.baz.a的变化,此时应在set时递归
if 动态追加属性,无法拦截 不触发get,set,此时应用this.$set()/Vue.set()function defineReactive (obj, key, val) {
// 循环obj的每一项,都触发响应式
observe(val)
Object.defineProperty(obj, key, {
get () {
console.log('get', key)
return val
},
set(v) {
console.log('set', key)
if (val !== v) {
// 这里递归 if是对象 监听对象内部值的变化
observe(v)
val = v
}
}
})
}
// 追加属性触发 set e.g.
// obj.newValue = 1 × 监听不到
// set(obj,newValue, 1) √
function set (obj, key, val) {
defineReactive(obj, key, val)
}复制代码
此时数据响应式分析完成,下面是代码// Object.defineProperty()
// 将传入的obj,动态设置一个key,它的值val
function defineReactive(obj, key, val) {
// 递归
observe(val)
Object.defineProperty(obj, key, {
get() {
console.log('get', key);
return val
},
set(v) {
if (val !== v) {
console.log('set', key);
// 传入新值v可能还是对象
observe(v)
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)
}
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复制代码
而Vue中无外乎多了一些编译步骤,例如
因为无法识别出foo,需要通过编译器去解析 {{}}
把 模板视图转换成更新函数 即解析 foo,触发update函数
就应用到 模板引擎, 渲染
2. 模板引擎模版引擎:提供描述视图的模版语法
插值:{{}}
指令:v-bind,v-on,v-model,v-for,v-if 等渲染:如何将模板转换为html模板 => vdom => dom
接下来就要实现一个简单的Vue,实现原理分析new Vue() ⾸先执⾏初始化,对data执⾏响应化处理,这个过程发⽣在 Observer 中
同时对模板执⾏编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发⽣在 Compile 中
同时定义⼀个 更新函数 和 Watcher ,将来对应数据变化时Watcher会调⽤更新函数
由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher(对应关系:1key:1Dep:nWatcher)将来data中数据⼀旦发⽣变化,会⾸先找到对应的Dep,通知所有Watcher执⾏更新函数
涉及类型介绍Jvue:框架构造函数
Observer:执⾏数据响应化(分辨数据是对象还是数组)
Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)
Watcher:执⾏更新函数(更新dom)
Dep:管理多个Watcher,批量更新
开始实现,首先创建一个index.html
{{counter}}
{{counter}}
{{counter}}
/* 生成vue实例 */
const app = new JVue({
el: '#app',
data: {
counter: 1,
desc: '村长真棒'
},
methods: {
onclick() {
this.counter++
}
},
})
setInterval(() => {
app.counter++
}, 1000);
复制代码
js部分
JVue
首先创建一个Jvue类,接收外部传递的数据,执⾏初始化,对data执⾏响应化处理function defineReactive(obj, key, val) {
// 递归
observe(val);
Object.defineProperty(obj, key, {
get() {
console.log("get", key);
return val;
},
set(v) {
if (val !== v) {
console.log("set", key);
// 传入新值v可能还是对象
observe(v);
val = v;
}
},
});
}
// 递归遍历obj,动态拦截obj的所有key
function observe(obj) {
if (typeof obj !== "object" || obj == null) {
return obj;
}
// 每出现一个对象,创建一个Ob实例 所以vue审查时,如果有_ob 即是相应式数据
new Observer(obj);
}
class JVue { // 框架构造函数
constructor(options) {
// 保存选项,方便在应用位置拿到值
this.$options = options;
this.$data = options.data;
// 2.响应式处理
observe(this.$data)
}
}
// Observer: 判断传入obj类型,做对应的响应式处理
class Observer {
constructor(obj) {
this.value = obj;
// 判断对象类型
if (Array.isArray(obj)) {
// todo
} else {
this.walk(obj);
}
}
// 对象响应式
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
}复制代码
代理data
此时,页面数据理论上绑定了,实际并没有,因为传入的我们其实想要相应式的是data,而不是实例,所以进行一次代理,代理data到JVue实例上// 代理时要注意,很容易出现实例和代理有相同的key,这里为了方便不做处理
function proxy(vm) {
Object.keys(vm.$data).forEach((key) => { // 遍历data 给vm挂上
Object.defineProperty(vm, key, {
get() {
return vm.$data[key];
},
set(v) {
vm.$data[key] = v;
},
});
});
}
class JVue {
constructor(options) {
// ...
// 3.代理data到JVue实例上
proxy(this);
}
}复制代码
此时,数据已绑定上,初始化数据已更新
编译 - Compile
编译模板中vue模板特殊语法,初始化视图、更新视图 (通过递归遍历节点)class JVue {
constructor(options) {
// ...
// 4.编译
new Compile(options.el, this);
}
}
class Compile {
// el-宿主,vm-JVue实例
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
this.compile(this.$el);
}
compile(el) {
// 遍历el dom树
el.childNodes.forEach((node) => {
if (this.isElement(node)) {
// element
// 需要处理属性和子节点
// console.log("编译元素", node.nodeName);
this.compileElement(node);
// 递归子节点
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
} else if (this.isInter(node)) {
// console.log("编译插值表达式", node.textContent);
// 获取表达式的值并赋值给node
this.compileText(node);
}
});
}
isElement(node) { // 节点是元素节点
return node.nodeType === 1;
}
// {{xxx}}
isInter(node) { // 节点是文本节点 并符合 {{}}
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
isDir(attr) {
return attr.startsWith("j-");
}
// 更新函数,
update(node, exp, dir) {
// init
const fn = this[dir + 'Updater']
fn && fn(node, this.$vm[exp])
}
// 编译文本,将{{ooxx}}
compileText(node) {
this.update(node, RegExp.$1, 'text')
}
textUpdater(node, val) {
node.textContent = val
}
// 处理元素所有动态属性
compileElement(node) {
Array.from(node.attributes).forEach((attr) => {
const attrName = attr.name;
const exp = attr.value;
// 判断是否是一个指令
if (this.isDir(attrName)) {
// 执行指令处理函数
// j-text, 截取text 触发指令方法
const dir = attrName.substring(2);
this[dir] && this[dir](node, exp)
}
});
}
// j-text处理函数
text(node, exp) {
this.update(node, exp, 'text')
}
// j-html
html(node, exp) {
this.update(node, exp, 'html')
}
htmlUpdater(node, val) {
node.innerHTML = val
}
}复制代码
此时已编译成功,但是触发频率一直触发get,set,我们想通过某个方法监听,真正发生改变才会触发变更函数,就用到了Watcher ,Dep
依赖收集
视图中会⽤到data中某key,这称为依赖。同⼀个key可能出现多次,每次都需要收集出来⽤⼀个
Watcher来维护它们,此过程称为依赖收集。
多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知。
实现思路defineReactive时为每⼀个key创建⼀个Dep实例
初始化视图时读取某个key,例如name1,创建⼀个watcher1
由于触发name1的getter⽅法,便将watcher1添加到name1对应的Dep中
当name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新// 依赖:和响应式对象的每个key一一对应
class Dep {
constructor() {
this.deps = []
}
addDep(dep) {
this.deps.push(dep)
}
notify() {
this.deps.forEach(dep => dep.update())
}
}
// 做dom更新
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm
this.key = key
this.updateFn = updateFn
// 创建watcher时触发getter 读取一下key的值,触发其get,从而收集依赖
Dep.target = this
this.vm[this.key]
Dep.target = null
}
update() {
this.updateFn.call(this.vm, this.vm[this.key])
}
}
class Compile {
// ...
// 更新函数,
update(node, exp, dir) {
// init
const fn = this[dir + 'Updater']
fn && fn(node, this.$vm[exp])
// update: 创建Watcher
new Watcher(this.$vm, exp, function(val) {
fn && fn(node, val)
})
}
}
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(v) {
if (val !== v) {
console.log("set", key);
// 传入新值v可能还是对象
observe(v);
val = v;
dep.notify()
}
},
});
}复制代码
搞定