渐进地了解渐进式框架Vue

v1-minimalist
原理:

Object.defineProperty劫持数据变换,更新dom
事件监听,改变数据


复制代码const obj = {}; const inputDom = document.querySelector('#input'); const spanDom = document.querySelector('#span');

Object.defineProperty(obj, ‘txt’, {
get() {},
set(newVal) {
inputDom.value = newVal;
spanDom.innerHTML = newVal;
}
})

inputDom.addEventListener(‘input’, (e) => {
obj.txt = e.target.value
})

复制代码看看效果

v2-observer
原理:

监听者Observer: 用来劫持数据变化,通知发布者Dep。
发布者Dep: 负责收集订阅者Watcher,当收到监听者Observer的通知时,传递订阅者Watcher
订阅者Watcher: 当收到发布者消息时,执行对应函数

发布者Dep

let uid = 0;

class Dep {
constructor() {
this.id = uid++;
this.subs = [];
}
// 添加订阅者
addSub(sub) {
this.subs.push(sub)
}
// 通知订阅者更新
notify() {
this.subs.forEach(sub => sub.update())
}
//
depend() {
Dep.target.addDep(this)
// 若是新Dep,则会触发addSub重新添加订阅
}
}

// 当指向当前活跃的Watcher => 执行get 便于收集依赖时(排除不必要的依赖)
Dep.target = null;
复制代码
订阅者Watcher

import Dep from ‘./Dep’

class Watcher {
constructor(vm, expOrFn, cb) {
this.depIds = {}; // 存储订阅者的id
this.vm = vm; // vue实例
this.expOrFn = expOrFn; // 订阅数据的key
this.cb = cb; // 数据更新回调
this.val = this.get(); // 首次实例,触发get,收集依赖
}
get() {
// 当前订阅者(Watcher)读取被订阅数据的值时,通知订阅者管理员收集当前订阅者
Dep.target = this;
// 执行一次get
const val = this.vm._data[this.expOrFn];
Dep.target = null;
return val
}
update() {
this.run()
}
run () {
const val = this.get();
if (val !== this.val || isObject(val)) {
this.val = val;
this.cb.call(this.vm, val);
}
}
addDep(dep) {
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this)
this.depIds[dep.id] = dep;
}
}
}

function isObject (obj) {
return obj !== null && typeof obj === ‘object’
}

export default Watcher;
复制代码
监听者Observer

class Observer {
constructor(value) {
this.value = value
this.dep = new Dep()
def(value, ‘ob’, this)
if (Array.isArray(value)) {
/// 数组,包装数组响应式方法
protoAugment(value, arrayMethods)
this.observeArray(value)
} else {
// 对象,遍历属性,劫持数据
this.walk(value)
}
}
walk(value) {
Object.keys(value).forEach(key => this.convert(key, value[key]))
}
convert(key, val) {
defineReactive(this.value, key, val)
}
observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}

function defineReactive(obj, key, val) {
const dep = new Dep();
// 递归添加数据劫持
let chlidOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
dep.depend();
if (chlidOb) {
chlidOb.dep.depend()
if (Array.isArray(val)) {
dependArray(val)
}
}
}
return val
},
set(newVal) {
if (newVal === val) return;
val = newVal;
chlidOb = observe(newVal);
dep.notify()
}
})
}
复制代码值得一提的是,defineProperty无法监听数组变化,这也是我们在使用vue初期,困扰的this.arr[index] = xxx不会更新页面的问题,必须在使用array的方法(经vue包装过)才能达到预期效果,下面试着改造下Array的方法。

observeArray

import { def } from ‘./util’

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

// 会修改原数组的方法
const methodsToPatch = [ ‘push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’, ‘sort’, ‘reverse’ ]

methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (…args) {
const result = original.apply(this, args)
const ob = this.ob
let inserted
switch (method) {
case ‘push’:
case ‘unshift’:
inserted = args
break
case ‘splice’:
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
ob.dep.notify()
return result
})
})

复制代码实际使用
const vm = new Vue({
data: {
txt: ‘’,
arr: []
},
});

inputDom.addEventListener(‘input’, e => vm.txt = e.target.value);

buttonDom.addEventListener(‘click’, e => vm.arr.push(1));

vm. w a t c h ( t x t , t x t = > s p a n D o m . i n n e r H T M L = t x t ) ; v m . watch('txt', txt => spanDom.innerHTML = txt); vm. watch(‘arr’, arr => span1Dom.innerHTML = arr);
复制代码看看效果

v3-template
v2需要开发者操作dom,这一点也不mvvm。向vue看齐,实现一个简单的模版compiler,处理模版;绑定数据;挂载dom;达到隔离dom操作的效果。
原理:

将模版字符串通过innerHTML生成dom树
遍历dom节点,解析指令(v-if/v-for/…),绑定数据({{…}}),挂载更新函数

parser

export default function parseHTML(template) {
const box = document.createElement(‘div’)
box.innerHTML = template
const fragment = nodeToFragment(box);
return fragment
}
export function nodeToFragment(el) {
const fragment = document.createDocumentFragment();
let child = el.firstChild;
while (child) {
fragment.appendChild(child);
child = el.firstChild
}
return fragment;
}

复制代码
patch

export default function patch(el, vm) {
const childNodes = el.childNodes;
[].slice.call(childNodes).forEach(function(node) {
const text = node.textContent;

  if (node.nodeType == 1) {  
    // 元素节点
    patchElement(node, vm);
  } else if (node.nodeType == 3) {
    // 文本节点
    patchText(node, vm, text);
  }

  if (node.childNodes && node.childNodes.length) {
    patch(node, vm);
  }

});
return el
}

export default function patchElement(node, vm) {
const nodeAttrs = node.attributes;
const nodeAttrsArr = Array.from(nodeAttrs)
nodeAttrsArr.forEach((attr) => {
const { name, value } = attr;
// 默认指令
if (dirRE.test(name)) {
if (bindRE.test(name)) { // v-bind
const dir = name.replace(bindRE, ‘’)
handleBind(node, vm, value, dir)
} else if (modelRE.test(name)) { // v-model
const dir = name.replace(modelRE, ‘’)
handleModel(node, vm, value, dir)
} else if (onRE.test(name)) { // v-on/@
const dir = name.replace(onRE, ‘’)
handleEvent(node, vm, value, dir)
} else if (ifArr.includes(name)) { // v-if
handleIf(node, vm, value, name)
} else if (forRE.test(name)) { // v-for
handleFor(node, vm, value)
}
node.removeAttribute(name);
}
})
return node
};

const defaultTagRE = /{{(.*)}}/

export default function patchText(node, vm, text) {
if (defaultTagRE.test(text)) {
const exp = defaultTagRE.exec(text)[1]
const initText = vm[exp];
updateText(node, initText);
new Watcher(vm, exp, (value) => updateText(node, value));
}
}

function updateText(node, value) {
node.textContent = isUndef(value) ? ‘’ : value;
}

复制代码
directives(举例说明)

export function handleBind (node, vm, exp, dir) {
const val = vm[exp];
updateAttr(node, val);
new Watcher(vm, exp, (value) => updateAttr(node, value));
}

const updateAttr = (node, attr, value) => node.setAttribute(attr, isUndef(value) ? ‘’ : value);

export function handleModel (node, vm, exp, dir) {
let val = vm[exp];
updateModel(node, val);
new Watcher(vm, exp, (value) => updateModel(node, value));
handleEvent(node, vm, (e) => {
const newValue = e.target.value;
if (val === newValue) return;
vm[exp] = newValue;
val = newValue;
}, ‘input’)
}

export function handleEvent (node, vm, exp, dir) {
const eventType = dir;
const cb = isFun(exp) ? exp : vm[exp].bind(vm);
if (eventType && cb) {
node.addEventListener(eventType, e => cb(e), false);
}
}

const updateModel = (node, value) => node.value = isUndef(value) ? ‘’ : value;

export function handleFor (node, vm, exp) {
const inMatch = exp.match(forAliasRE)
if (!inMatch) return;
exp = inMatch[2].trim();
const alias = inMatch[1].trim();
const val = vm[exp];
const oldIndex = getIndex(node);
const parentNode = node.parentNode;
parentNode.removeChild(node);
node.removeAttribute(‘v-for’);
const templateNode = node.cloneNode(true);
appendForNode(parentNode, templateNode, val, alias, oldIndex);
new Watcher(vm, exp, (value) => appendForNode(parentNode, templateNode, val, alias, oldIndex));
}

function appendForNode(parentNode, node, arr, alias, oldIndex) {
removeOldNode(parentNode, oldIndex)
for (const key in arr) {
const templateNode = node.cloneNode(true)
const patchNode = patch(templateNode, {[alias]: arr[key]})
patchNode.setAttribute(‘data-for’, true)
parentNode.appendChild(patchNode)
}
}
复制代码现在,我们用模版试下效果
let vm = new Vue({
el: ‘#app’,
template:
`



<input @input=“input” type=“text”/>





<button @click=“addArr”>数组+1






`, data: { txt: '', arr: [1, 2, 3] }, methods: { input(e) { const newValue = e.target.value; if (this.txt === newValue) return; this.txt = newValue; }, addArr() { this.arr.push(this.arr.length + 1) } } }); 复制代码 v4-vdom 作为消费级的框架而不是玩具(呵呵!依然是玩具。。。),当然是希望在保证可开发维护同时,我们的性能要过得去。 显然,因为数据变化而频繁地更新dom,不是我们想要。vue给的方案是VNode(对象的方式描述dom) 原理:

parse:将模板编译成AST
generate:根据AST,拼接成函数字符串,通过new Function构造render函数(借用with延长作用域)

例:

借助Vnode的创建函数,执行render生成虚拟DOM树
通过diff算法,对比出更新的dom操作,执行更新。vue的diff算法参考了sanbbdom,想了解diff算法的发展历程可以参考部分diff算法演进

parse代码有点琐碎,可以直接看源码
generate

export function generate (ast) {
const code = ast ? genElement(ast) : ‘_c(“div”)’
return {
render: with(this){return ${code}}
}
}

export function genElement (el) {
if (el.for && !el.forProcessed) {
return genFor(el)
} else if (el.if && !el.ifProcessed) {
return genIf(el)
} else {
let code
let data
if (!el.plain) {
data = genData(el)
}

const children = genChildren(el, true)
code = `_c('${el.tag}'${
  data ? `,${data}` : '' // data
}${
  children ? `,${children}` : '' // children
})`
return code

}
}

复制代码
patch
大致规则如下:

只针对同层级节点对比(降低复杂度)
双端比较,找到末端位置相同的节点(找到操作次数最少的更新路线)
双端比较后未找到相同节点则遍历查找
判断两个是否为同节点(sameVnode:a.key === b.key && a.tag === b.tag),否则删除旧节点,创建新节点;是则执行4
文本节点则替换文本,元素节点则比较子节点(递归)

function updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[–oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) { // oldStart == newStart 更新节点
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) { // oldEnd == newEnd 更新节点
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[–oldEndIdx]
newEndVnode = newCh[–newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // oldStart == newEnd 更新节点 节点右移
patchVnode(oldStartVnode, newEndVnode)
nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[–newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // oldStart == newEnd 更新节点 节点左移
patchVnode(oldEndVnode, newStartVnode)
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[–oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode)
oldCh[idxInOld] = undefined
nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 相同的键,但不同的元素。当作新元素对待
createElm(newStartVnode, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) { // 需要新增节点
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx)
} else if (newStartIdx > newEndIdx) { // 需要移除节点
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
复制代码看一下分解动作:
http://weavi.com/17046580
http://weavi.com/17046574
http://weavi.com/17078782
http://weavi.com/17078781
http://weavi.com/17078780
从sameVnode判断上不难看出,在v-for循环出的列表的场景中,对元素设置key,直接指导diff是否复用DOM。
敲黑板,这里指出两个我们编写时的问题

使用index作为key值(为了骗过idea?)与不设置key效果其实相同,这两种情况下,都会复用DOM。因为index === index(undefind === undefind);
需要对列表进行增删或改变顺序时,建议设定独特的id作为key,这样可以最大限度指导diff,同时避免错误渲染;

写到这里一步,完成了vue的基本操纵,剩下扩展component/filter/mixin/生命周期等等特性就不一一分解了。
以上代码主要为了描述vue运行过程,部分借鉴vue源码,但丢失了很多细节,有兴趣的同学可以参考vue源码分析。
事实上,虚拟dom的意义远非提高性能这么简单。我们有了描述UI的规则后,单从vue来讲,不依赖常规宿主环境,可以是浏览器,是weex,或者node跑ssr;从大环境来讲,这为原生级跨端提供可能,比如RN;当然也有从编译上阶段实现跨平台的,比如Taro/uniapp。
关于下一版,参考vue3.x,实现一些新特性。
聊一聊Vue3+
数据劫持的痛点
前面提到Vue2.x采用defineProperty劫持数据,这个做法有两个问题。
一是需要初始化时,遍历递归一一定义OB;
二是无法劫持数组的变化,倒不是没有方案劫持数组,基于性能考量,Vue采用了改造数组方法的方式;
复制代码
Vue3.0采用了新的劫持方案Proxy,一次性解决上述问题。但就目前国内环境而言,依然存在大量低版本ie用户,兼容版还会沿用2.x的机制
逻辑复用的历程

mixins

✅ 解决的问题:
将任意个组件特征(属性和方法)拷贝到需要的组件中,达到复用的目的
复制代码
❌ 造成的困扰:
当多个mixins配合时,会出现数据源不清晰和命名可能冲突的问题
复制代码

slot-scope

✅ 解决的问题:
让组件通用功能得到封装,而不同逻辑通过插槽分发
复制代码
❌ 造成的困扰:
多层组件嵌套时,无法清晰的体现具体是哪个组件在模板中提供哪个变量。
需要额外实例组件,造成额外性能开销
复制代码

HOC

✅ 解决的问题:
秉承分层的思想,可以处理和分发传入的参数和方法
复制代码
❌ 造成的困扰:
来自民间的用法,相较与React,Vue的HOC使用起来尤为鸡肋。
因为原来的父子组件关系被分割,产生了属性和方法以及真实ref传递问题,比如v-model之类的,都需要高阶组件手动处理。
与slot-scope类似,因为需要额外的实例组件而造成性能开销
复制代码

Function-based API

✅ 解决的问题(案例)
从官方给出的案例来看,确实不存在上述方案造成的副作用。
至于是否会像社区所反应的,基于函数的 API 会造成大量面条代码产生,这就需要大家实践了才知道了。
复制代码
关于下一代
以下内容,纯属个人YY,不喜轻喷。
关于下一代,React已经指明了一个小方向—Fiber。且先不谈它的出现会不会像vdom一样为前端带来革命性的性能提升,单单循环任务调度的思路就很契合js的开发思路,Vue会不会借鉴暂时还不清楚,但至少会有适合Vue的方案出现。
在编译阶段做更多文章,在开发者和机器之间做更多,一方面能让开发者更加专注逻辑而不是代码组织;另一方面提高运行时的效率,借鉴一个现下很热门的例子—WebAssembly,当然编译成机器更易于理解和执行的代码,势必让框架编写更多的判断来解决适配以及线上调试难以定位等等问题。合理分割compiler和runtime的代码也是框架必须思考的问题。
然后是Service Worker,目前看真正得到广泛应用的还是PWA方面,相信在Google的进一步推广下(Apple依然会从中作梗),成为标准也将会在各大框架中得到应用,比如把diff放到WebWorker中去。这远比小程序的思路—双线程要来得有意思的多,当然我还是尊重小程序作为平台向的作用。只是各家小程序接口和质量不一,没有标准,要坐等小程序消费大户—JD继续探索。。。

发布了60 篇原创文章 · 获赞 10 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/a59612/article/details/102943044