vue数据层思路_vue2.0源码分析 简单实现new Vue(1)

作为前端小白,日常搬砖,写的一般都是业务代码,对底层的实现原理一知半解。

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 的数据,从而实现双向绑定数据劫持。

vue数据层思路_vue2.0源码分析 简单实现new Vue(1)_第1张图片

将视图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,需要通过编译器去解析 {{}}

把 模板视图转换成更新函数  即解析 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()

}

},

});

}复制代码

搞定

你可能感兴趣的:(vue数据层思路)