一直没仔细研究过这个,感觉自己还是太不够敏感了,原来只是看到有双向绑定,没细想过原理。
my git
hijack方式
hijack主要分为两部分:对数据的劫持、与DOM的关联。两个原理听起来都很简单写起来都是各种小坑
数据劫持
git
原理很简单,利用Object.defineProperty对数据进行劫持
坑1. hijack obj必须在 hijack propert之前,不然会触发多次劫持后的getter方法,当然实际上getter里只有一个绑定,所以也没啥问题
const hijackObj = function hijackObj (obj) {
if (isObj(obj)) {
Object.keys(obj).forEach((key) => {
// if hijack property first, would trigger getter several times!
if (isObj(obj[key])) {
hijackObj(obj[key])
}
hijackProperty(obj, key, obj[key])
})
}
}
const hijackProperty = function hijackProperty (obj, key, val) {
Object.defineProperty(obj, key, {
configurable: false,
get () {
// do sth here
return val
},
set (nVal) {
// do sth here
val = nVal; // eslint-disable-line
}
})
}
写法和vue的写法不太一样,主要是函数定义和分层上修改了下,我觉得这样更明确,如果要看vue的话可以看参考文档里的
除此之外,为了暴露对象(在vue中是vm),还定义了一个全局变量,同样采用劫持的方式对其进行修改。由于这个对象并不是原本对象,且原本对象深层的修改也会触发表层的改变,因此只需要劫持最顶上一层就可以了
Object.keys(obj).forEach((key) => {
Object.defineProperty(window.hm, key, {
configurable: false,
get () {
return obj[key]
},
set (nVal) {
obj[key] = nVal;
}
})
})
此时对window.hm的修改就是对原本obj的修改,原本obj的修改也会在window.hm上显现出来
数据触发
双向绑定的核心点之一是数据的更新,现在,数据修改时已经可以在setter中获取到并处理,那就可以通过观察者模型在setter中触发一个更新事件。
const hijackProperty = function hijackProperty (obj, key, val) {
// 每一个property都有一个model
const model = new Model()
Object.defineProperty(obj, key, {
configurable: false,
get () {
// do sth here
return val
},
set (nVal) {
model.trigger('val-change', nVal)
val = nVal
}
})
}
Model是自定义的一个事件类,每个属性都有一个model,从而成为一个监听者,当元素发生改变时触发闭包内model的’val-change’事件
显然,观察者应该在关联DOM时传入,这点上Vue的传入方式非常巧妙……
DOM关联
git
DOM关联是hijack的另一个重要组成部分,负责找到所有需要变化的DOM并绑定成为观察者
const compileNode = function compileNode (ele) {
Array.from(ele.childNodes).forEach((node) => {
// text node
if (node.nodeType === 3) {
compileTextNode(node)
return
} else {
compileEleNode(node)
}
if (node.childNodes && node.childNodes.length) {
compileNode(node)
}
})
}
以text节点的绑定为例
这段代码只实现了简单的单一property绑定,且没有绑定到深层
const compileTextNode = function compileTextNode (node) {
const pattern = /\{\{(.*)\}\}/
const match = pattern.exec(node.nodeValue)
if (!match) {
return
}
const rawVal = node.nodeValue
const prop = match[1]
// bind to watcher and replace here
window.hm.target = (val) => {
node.nodeValue = rawVal.replace(pattern, val)
}
node.nodeValue = rawVal.replace(pattern, obj[prop])
window.hm.target = null
}
rawVal.replace(pattern, obj[prop])会触发obj[prop]的getter事件,在这个时候就可以将观察者注入到其model对象中
const hijackProperty = function hijackProperty (obj, key, val) {
// 每一个property都有一个model
const model = new Model()
Object.defineProperty(obj, key, {
configurable: false,
get () {
// 利用全局变量传递cb进来
if (window.hm.target) {
model.on('val-change', window.hm.target)
}
return val
},
set (nVal) {
model.trigger('val-change', nVal)
val = nVal
}
})
}
采用全局对象的方式,可以在getter中判断这次获取是否需要添加观察者,从而实现观察者的注入
Vue采用的是Dep构造函数的属性判断,可以看参考文档的写法,实质上也是一个全局对象
首次遍历的时候会有多次DOM操作与替换,为了减少reflow的性能损耗,在外层采用fragment
const compile = function compile (ele) {
if (!ele) {
return
}
let fragment = transEleToFragment(ele)
compileNode(fragment)
ele.appendChild(fragment)
}
在启动的时候调用一次bind(obj)
实现数据劫持、使用compile(document.body)
实现DOM关联,这样就可以实现数据的双向绑定了,而且可以通过window.hm获取到实时的数据变化
其他
Vue采用的还有一层Watcher,作为数据更新时的中间发布层,相当于一个发布订阅模型
我觉得没有必要……所以没写
脏检查
// TODO